From af8fea7c4f3e542f1a5fc3e240213b341f58d62b Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Tue, 1 Sep 2026 13:19:11 +0200 Subject: [PATCH 01/65] 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 0f1657df2ac4fbb3b8878c16153b4c8cc8a0ee49 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Tue, 1 Sep 2026 13:55:18 +0200 Subject: [PATCH 02/65] 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 e879da2c181411d389c3584c4c0063912ffe22c5 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 06:18:49 +0200 Subject: [PATCH 03/65] 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 7ae3e54ccd44634b8eed03374b09e0294e5d85c7 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 06:23:16 +0200 Subject: [PATCH 04/65] 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 541fd73969a1600cd05fd405b8db60e2dd678b05 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 06:29:37 +0200 Subject: [PATCH 05/65] 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 ebe2a3467903b8deb0c156b9d1bff4e79b96f5db Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 06:31:21 +0200 Subject: [PATCH 06/65] 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 4398694fe4f7092e7f67343704b567e206c1735a Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 06:32:08 +0200 Subject: [PATCH 07/65] 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 63d5c1e02710b82daa1168f4972d2a394b7f9d36 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 06:35:40 +0200 Subject: [PATCH 08/65] 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 64c73067d5315840d6d621e6f5eea2e659cc9d81 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 06:37:52 +0200 Subject: [PATCH 09/65] 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 ac087ad1deac75c89e142cc6db16f3238e3bea94 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 07:01:04 +0200 Subject: [PATCH 10/65] 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 156a2127d1cf632c2cbcb46716a58bf3a20a2336 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 08:26:27 +0200 Subject: [PATCH 11/65] 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 e77fedc7f49e0f56cd493c314b8a5b590cfacd6a Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 08:28:44 +0200 Subject: [PATCH 12/65] 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 825adf33249d4b9157447f12be6bf8e62de1ee03 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 08:29:54 +0200 Subject: [PATCH 13/65] 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 d583e261eb6bdef6e73e4e2b4165d2c142fb1c6f Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 09:06:57 +0200 Subject: [PATCH 14/65] 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 7e8817f53c11239b11966a4452ce9d7942ff1592 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 09:08:14 +0200 Subject: [PATCH 15/65] 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 45d3838a9c8838d09890e038470c83ac9dfab478 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 09:10:33 +0200 Subject: [PATCH 16/65] 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 c01f3f8b6b53b418f8e1b9db7f63f3d5294b7fbe Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 09:29:27 +0200 Subject: [PATCH 17/65] 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 77cc1a71c20cc6cdf65305f6652a1b36f951c534 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 09:30:08 +0200 Subject: [PATCH 18/65] 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 6693cbabfaa5016b6ac2d8174aa8d94413620155 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 09:31:20 +0200 Subject: [PATCH 19/65] 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 1633b042d1368f4e725c8e6d3b8270c6625b151b Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 09:52:49 +0200 Subject: [PATCH 20/65] 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 ff75fe93ebf5f37fc8a56d43cbdad072fa91ca03 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 09:55:57 +0200 Subject: [PATCH 21/65] 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 772f720173e2c4454fc0e6557052f34a95557f9c Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 09:56:26 +0200 Subject: [PATCH 22/65] 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 d427a47a8cac6dc9fb7ea414bb876817d516e4a8 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 09:58:00 +0200 Subject: [PATCH 23/65] 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 b611c8e1d72b135229113fb7962228fd124586f7 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 10:00:17 +0200 Subject: [PATCH 24/65] 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 b328dbad7f12c1ef8b269f5ccbaaf92bac6b5bf6 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 10:02:29 +0200 Subject: [PATCH 25/65] 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 a429ea6eb32b8420107c962104276f6d3443da1f Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 10:04:26 +0200 Subject: [PATCH 26/65] 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 320ae929566f7ecce81c70be7425f89ad89843ae Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 10:08:08 +0200 Subject: [PATCH 27/65] 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 464fb8226512ad6e026bb9f97cddd08efdf3e89a Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 10:10:02 +0200 Subject: [PATCH 28/65] 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 cec2a0338c183097f05e78e32052084b5f00c99d Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 11:16:47 +0200 Subject: [PATCH 29/65] 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 753afccf8674fd7a997e5765cbd1f8c7fe1717ec Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 11:23:50 +0200 Subject: [PATCH 30/65] 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 cf93ddcfddfe1095527e0da3f29409775e6a09e0 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 12:15:38 +0200 Subject: [PATCH 31/65] 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 78edb9c06dbb58bb93821baf26cc7a147db12590 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 12:16:11 +0200 Subject: [PATCH 32/65] 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 b67e2214c04a96619c84d4dd7ec32697b291f851 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 12:16:57 +0200 Subject: [PATCH 33/65] 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 bc9060b740e3758684b84f64796150c9d418b76f Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 12:18:26 +0200 Subject: [PATCH 34/65] 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 30445a96998f18a2233022e95009105665bd8557 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 12:20:14 +0200 Subject: [PATCH 35/65] 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 dc57add8747443e945291e348530838dc5b93dca Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 12:22:08 +0200 Subject: [PATCH 36/65] 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 d4c3b4361847e85ed23e23c124ca130952f6c58e Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 12:50:56 +0200 Subject: [PATCH 37/65] 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 827ce542e17f230f6556222baae88b2823f805f5 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 13:01:55 +0200 Subject: [PATCH 38/65] 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 265466c25e3211b30c55f1f46fda5a5d54aab838 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 13:45:25 +0200 Subject: [PATCH 39/65] 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 08e46312ef390268d2c0707e50f5796ad3aa93ce Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 14:57:45 +0200 Subject: [PATCH 40/65] 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 cbbadd3d44d7854a53398a73a7f2eaf9eb8bc985 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 15:48:16 +0200 Subject: [PATCH 41/65] 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 6a3a0fb874e492424bea6cdaf4f2ff5c33e8a8ca Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 16:02:43 +0200 Subject: [PATCH 42/65] 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 913423754473d96c345d89df5556d15e6e57bfea Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 20:41:48 +0200 Subject: [PATCH 43/65] 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 cea51b521727a520c2b677bb70e5968d8e0f9bbd Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 20:43:19 +0200 Subject: [PATCH 44/65] 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 91b059f68a9a6fef3d7a0b1f8ace0c51078c77f1 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 20:43:53 +0200 Subject: [PATCH 45/65] 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 4f45709a64f4d0ec7fc02cbaa93308d85fd7a41f Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 20:46:23 +0200 Subject: [PATCH 46/65] 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 696b5e45fb7a7c539931e5e260957f1d8d7fc5ae Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 20:48:39 +0200 Subject: [PATCH 47/65] 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 5531f5552d0cf0848b967cb909dee6951c23cf6f Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 20:49:57 +0200 Subject: [PATCH 48/65] 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) From 06a065ba7c2744da2e5e6a37118c4f35ab36dfd0 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Thu, 10 Sep 2026 07:15:36 +0200 Subject: [PATCH 49/65] cbor: honor debug_dissector and slim CBOR packet init Align BYTE_STRING_PACKET with other nested-packet exception handling, reduce do_init_cached_fields to packet re-parenting, cover Packet ismutable defaults via FlagsField, and drop leftover is_absent / redundant ANY.build helpers. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborfields.py | 24 ++++++++---------------- scapy/cborpacket.py | 17 +++-------------- test/fields.uts | 13 +++++++++++++ test/scapy/layers/cbor.uts | 38 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 30 deletions(-) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 0fadee454a8..f6dce337725 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -394,11 +394,6 @@ def mark_absent(self, pkt): """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) @@ -590,13 +585,6 @@ def any2i(self, pkt, x): return x return self.python_to_cbor_object(x) - def build(self, pkt): - # type: (CBOR_Packet) -> bytes - val = pkt.getfieldval(self.name) - if val is CBOR_ABSENT: - return b"" - return self.i2m(pkt, val) - def _build_counted(self, pkt): # type: (CBOR_Packet) -> _CBORBuildResult val = pkt.getfieldval(self.name) @@ -824,7 +812,11 @@ def _decode_packet_value(self, pkt, data): return packet.Raw(data) try: return pkt_cls(data, _parent=pkt) # type: ignore + except CBOR_Decoding_Error: + raise except Exception as exc: + if config.conf.debug_dissector: + raise raise CBOR_Decoding_Error( "Failed to decode byte-string packet content: %s" % exc ) from exc @@ -2029,7 +2021,7 @@ def name(self): return self.inner_field.name def _parse_tag_head(self, s): - # type: (bytes) -> Tuple[int, bytes] + # type: (bytes) -> bytes try: major_type, tag_num, remaining = CBOR_decode_head(s) except CBOR_Codec_Decoding_Error as e: @@ -2040,7 +2032,7 @@ def _parse_tag_head(self, s): if tag_num != self.tag_num: raise CBOR_Type_Mismatch( "Expected tag %d, got %d" % (self.tag_num, tag_num)) - return tag_num, remaining + return remaining def _encode_tagged(self, inner_data): # type: (bytes) -> bytes @@ -2061,7 +2053,7 @@ def matches_next_item(self, pkt, s): def _dissect_counted(self, pkt, s): # type: (CBOR_Packet, bytes) -> _CBORParseResult - _tag_num, remaining = self._parse_tag_head(s) + remaining = self._parse_tag_head(s) inner = self.inner_field._dissect_counted(pkt, remaining) if inner.items != 1: raise CBOR_Decoding_Error( @@ -2078,7 +2070,7 @@ def _build_counted(self, pkt): def _parse_value(self, pkt, s): # type: (CBOR_Packet, bytes) -> _CBORParseResult - _tag_num, remaining = self._parse_tag_head(s) + remaining = self._parse_tag_head(s) inner = self.inner_field._parse_value(pkt, remaining) if inner.items != 1: raise CBOR_Decoding_Error( diff --git a/scapy/cborpacket.py b/scapy/cborpacket.py index 3da0fae8320..78629a6e3be 100644 --- a/scapy/cborpacket.py +++ b/scapy/cborpacket.py @@ -82,21 +82,10 @@ def do_init_cached_fields(self, for_dissect_only=False): ) if for_dissect_only: return - # Packet copies ismutable defaults into fields; promote leftovers and - # re-parent nested packet defaults onto this instance. - for f in self.fields_desc: + # Packet copies ismutable defaults; only re-parent nested packets here. + for f in self.packetfields: 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 + self.fields[f.name] = f.any2i(self, self.fields[f.name]) def _raw_packet_cache_field_value(self, fld, val, copy=False): # type: (Any, Any, bool) -> Optional[Any] diff --git a/test/fields.uts b/test/fields.uts index e2d1132d414..3d510036741 100644 --- a/test/fields.uts +++ b/test/fields.uts @@ -1704,6 +1704,19 @@ with warnings.catch_warnings(record=True) as w: ######## + FlagsField += ismutable FlagsField defaults are isolated between packet instances + +class IsolatedFlagsPkt(Packet): + fields_desc = [ + FlagsField("flags", 0, 8, ["A", "B", "C"]) + ] + +a = IsolatedFlagsPkt() +b = IsolatedFlagsPkt() +a.flags.A = True +assert int(a.flags) == 1 +assert int(b.flags) == 0 + = Test Flags Field Iterator class FlagsTest(Packet): diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index 347049019b0..d88c108f72e 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -2598,6 +2598,44 @@ assert parsed.child.load == b"x" assert parsed.child.parent is parsed assert parsed.child.underlayer is None += CBORF_BYTE_STRING_PACKET honors conf.debug_dissector for child TypeError +from scapy.config import conf +from scapy.cbor.cbor import CBOR_Decoding_Error +from scapy.cbor.cborfields import CBORF_BYTE_STRING_PACKET, CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class BspTypeErrorChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) + def __init__(self, *args, **kwargs): + raise TypeError("intentional byte-string packet failure") + +class BspTypeErrorParent(CBOR_Packet): + CBOR_root = CBORF_BYTE_STRING_PACKET( + "child", None, pkt_cls=BspTypeErrorChild + ) + +# CBOR byte string wrapping one unsigned integer: 0x41 0x01 +wire = b"\x41\x01" +_old_dbg = conf.debug_dissector +try: + conf.debug_dissector = False + try: + BspTypeErrorParent(wire) + except CBOR_Decoding_Error as exc: + assert "intentional byte-string packet failure" in str(exc) + else: + raise AssertionError("TypeError was not wrapped as CBOR_Decoding_Error") + + conf.debug_dissector = True + try: + BspTypeErrorParent(wire) + except TypeError as exc: + assert "intentional byte-string packet failure" in str(exc) + else: + raise AssertionError("TypeError was hidden despite debug_dissector") +finally: + conf.debug_dissector = _old_dbg + + packet-valued collection ownership = CBORF_ARRAY_OF construction attaches every packet child to parent From ce48ad14b2e033e2eddd6b648c863e9d07772312 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Thu, 10 Sep 2026 09:12:52 +0200 Subject: [PATCH 50/65] cbor: compose nested and collection raw-cache fingerprints Snapshot nested CBOR packet fields by recursive fingerprints instead of shallow dict copies, and fingerprint ARRAY_OF/ANY and unknown-map pairs so wire-sensitive float mutations invalidate the parent cache. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborfields.py | 25 ++++++++++++++++++++++- scapy/cborpacket.py | 41 +++++++++++++++++++++++++++++++++++++- test/scapy/layers/cbor.uts | 41 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 2 deletions(-) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index f6dce337725..8d4528790f5 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -369,7 +369,9 @@ def do_copy(self, x): if isinstance(x, CBOR_UNDEFINED): return x if isinstance(x, list): - return copy.deepcopy(x) + return [self.do_copy(item) for item in x] + if isinstance(x, dict): + return {key: self.do_copy(value) for key, value in x.items()} if hasattr(x, "copy"): try: return x.copy() @@ -1436,6 +1438,16 @@ def __init__(self, self.holds_packets = 1 super(_CBORF_HOMOGENEOUS, self).__init__(name, default) + def cache_fingerprint(self, x): + # type: (Any) -> Any + """Compose item fingerprints when the element field provides them.""" + if self.holds_packets or self.item_field is None or x is None: + return None + item_fp = getattr(self.item_field, "cache_fingerprint", None) + if item_fp is None: + return None + return tuple(item_fp(item) for item in x) + @staticmethod def _require_packet_cls(pkt_cls): # type: (Any) -> Type[CBOR_Packet] @@ -1741,6 +1753,17 @@ def do_copy(self, x): # type: ignore[override] # type: (Any) -> Any return copy.deepcopy(x) + def cache_fingerprint(self, x): + # type: (Any) -> Any + """Wire-sensitive fingerprint for unknown text-key extension pairs.""" + if not x: + return () + fingerprint = CBORF_ANY._cache_fingerprint + return tuple( + (fingerprint(key), fingerprint(value)) + for key, value in x + ) + def is_empty(self, pkt): # type: (CBOR_Packet) -> bool return not pkt.getfieldval(self.name) diff --git a/scapy/cborpacket.py b/scapy/cborpacket.py index 78629a6e3be..47e65da138d 100644 --- a/scapy/cborpacket.py +++ b/scapy/cborpacket.py @@ -93,7 +93,46 @@ def _raw_packet_cache_field_value(self, fld, val, copy=False): # that semantic CBOR_Object equality ignores. cache_fingerprint = getattr(fld, "cache_fingerprint", None) if cache_fingerprint is not None: - return cache_fingerprint(val) + fingerprint = cache_fingerprint(val) + if fingerprint is not None: + return fingerprint + if fld.holds_packets: + # Compose nested CBOR field fingerprints instead of shallow-copying + # child.fields (which aliases mutable CBOR_Object trees). + def _child_fp(child): + # type: (Packet) -> Tuple[Any, Any] + child_fields = {} # type: Dict[str, Any] + if isinstance(child, CBOR_Packet): + from scapy.cbor.cborfields import CBOR_ABSENT + for cf in child.fields_desc: + if cf.name not in child.fields: + continue + cval = child.fields[cf.name] + if cval is CBOR_ABSENT: + child_fields[cf.name] = CBOR_ABSENT + continue + if cval is None and getattr(cf, "isconditional", False): + continue + if ( + cf.islist + or cf.holds_packets + or getattr(cf, "ismutable", False) + ) and cval is not None: + child_fields[cf.name] = \ + child._raw_packet_cache_field_value( + cf, cval, copy=copy + ) + else: + child_fields[cf.name] = cval + else: + child_fields = ( + fld.do_copy(child.fields) if copy else child.fields + ) + return (child_fields, child.payload.raw_packet_cache) + + if fld.islist: + return [_child_fp(item) for item in val] + return _child_fp(val) return super(CBOR_Packet, self)._raw_packet_cache_field_value( fld, val, copy ) diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index d88c108f72e..21ef246f338 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -685,6 +685,47 @@ assert pkt.raw_packet_cache == b"\x82\x01\x02" pkt.value.val.append(CBOR_UNSIGNED_INTEGER(3)) assert bytes(pkt) == b"\x83\x01\x02\x03" += Nested packet in-place ANY mutation invalidates the parent raw cache +from scapy.cbor.cborfields import CBORF_ANY, CBORF_PACKET +from scapy.cbor.cbor import CBOR_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class NestedAnyChild(CBOR_Packet): + CBOR_root = CBORF_ANY("value", None) + +class NestedAnyParent(CBOR_Packet): + CBOR_root = CBORF_PACKET("child", None, NestedAnyChild) + +pkt = NestedAnyParent(bytes.fromhex("8101")) +pkt.child.value.val.append(CBOR_UNSIGNED_INTEGER(2)) +assert bytes(pkt.child) == bytes.fromhex("820102") +assert bytes(pkt) == bytes.fromhex("820102") + += ARRAY_OF(CBORF_ANY) same-value float mutation invalidates raw cache +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_ANY +from scapy.cborpacket import CBOR_Packet + +class AnyArrayFloatPkt(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF("values", [], CBORF_ANY) + +pkt = AnyArrayFloatPkt(bytes.fromhex("81f90000")) +pkt.values[0].val = -0.0 +assert bytes(pkt) == bytes.fromhex("81f98000") + += Unknown map float mutation invalidates raw cache +from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER, CBORF_optional +from scapy.cborpacket import CBOR_Packet + +class UnknownFloatMap(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_optional(CBORF_UNSIGNED_INTEGER("a", None)), + ) + +pkt = UnknownFloatMap(bytes.fromhex("a16178f90000")) +assert pkt._cbor_unknown[0][0] == "x" +pkt._cbor_unknown[0][1].val = -0.0 +assert bytes(pkt) == bytes.fromhex("a16178f98000") + = Typed map lookup distinguishes integer 1 from boolean True from scapy.cbor import CBOR_Codecs from scapy.cbor.cbor import CBORMapData From 05847a29e6ae228f5fec0c1c684973c27115268e Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Thu, 10 Sep 2026 09:15:04 +0200 Subject: [PATCH 51/65] cbor: separate structural parse budgets from live cardinality Pre-count sequences and indefinite arrays with structural bounds so count_from and conditionals are not evaluated against defaults, and recompute live suffix reservations as discriminators are dissected. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborfields.py | 65 +++++++++++++++++++++++++++++++------ test/scapy/layers/cbor.uts | 66 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 10 deletions(-) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 8d4528790f5..d635f2e414a 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -205,6 +205,16 @@ def max_items(self, pkt): # type: (CBOR_Packet) -> int return 1 + def structural_min_items(self, pkt): + # type: (CBOR_Packet) -> int + """Lower bound independent of not-yet-dissected discriminators.""" + return self.min_items(pkt) + + def structural_max_items(self, pkt): + # type: (CBOR_Packet) -> int + """Upper bound independent of not-yet-dissected discriminators.""" + return self.max_items(pkt) + ########################## # Basic CBOR Field # @@ -1197,13 +1207,12 @@ 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 = suffix_mins[index + 1] + # Live suffix reservation so count_from / conditionals see wire + # discriminators dissected earlier in this pass. + 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: @@ -1279,11 +1288,13 @@ def _build_counted(self, pkt): 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. + # Count only up to this schema's structural max so trailing CBOR items + # remain for a parent, without freezing count_from on defaults. try: item_count = cbor_count_items( - s, max_count=self.max_items(pkt), until_break=False + s, + max_count=self.structural_max_items(pkt), + until_break=False, ) except CBOR_Codec_Decoding_Error as e: raise CBOR_Decoding_Error(str(e)) @@ -1298,6 +1309,14 @@ def max_items(self, pkt): # type: (CBOR_Packet) -> int return sum(f.max_items(pkt) for f in self.seq) + def structural_min_items(self, pkt): + # type: (CBOR_Packet) -> int + return sum(f.structural_min_items(pkt) for f in self.seq) + + def structural_max_items(self, pkt): + # type: (CBOR_Packet) -> int + return sum(f.structural_max_items(pkt) for f in self.seq) + class CBORF_ARRAY(_CBORF_compound): """ @@ -1354,7 +1373,9 @@ def _dissect_counted(self, pkt, 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. - child_max = sum(f.max_items(pkt) for f in self.seq) + child_max = sum( + f.structural_max_items(pkt) for f in self.seq + ) try: item_count = cbor_count_items( remaining, @@ -1662,6 +1683,14 @@ def max_items(self, pkt): return self.min_items(pkt) return self._list_limit() + def structural_min_items(self, pkt): + # type: (CBOR_Packet) -> int + return 0 + + def structural_max_items(self, pkt): + # type: (CBOR_Packet) -> int + return self._list_limit() + class CBORF_ARRAY_OF(_CBORF_HOMOGENEOUS): """ @@ -2172,6 +2201,14 @@ def max_items(self, pkt): # type: (CBOR_Packet) -> int return self._field.max_items(pkt) + def structural_min_items(self, pkt): + # type: (CBOR_Packet) -> int + return 0 + + def structural_max_items(self, pkt): + # type: (CBOR_Packet) -> int + return self._field.structural_max_items(pkt) + class CBORF_CONDITIONAL(CBORF_element, fields.ConditionalField): """ @@ -2218,6 +2255,14 @@ def max_items(self, pkt): return self.fld.max_items(pkt) return 0 + def structural_min_items(self, pkt): + # type: (CBOR_Packet) -> int + return 0 + + def structural_max_items(self, pkt): + # type: (CBOR_Packet) -> int + return self.fld.structural_max_items(pkt) + class CBORF_PACKET(CBORF_field['CBOR_Packet']): """ diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index 21ef246f338..3254cd0886c 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -1052,6 +1052,72 @@ pkt = CountedSeq(b"\x83\x02\x0a\x0b") assert pkt.n == 2 assert pkt.vals == [10, 11] += Unframed SEQUENCE with count_from uses the dissected discriminator +from scapy.cbor.cborfields import ( + CBORF_INTEGER, + CBORF_SEQUENCE, + CBORF_SEQUENCE_OF, +) +from scapy.cborpacket import CBOR_Packet + +class CountedUnframed(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_INTEGER("n", 0), + CBORF_SEQUENCE_OF( + "values", [], CBORF_INTEGER, count_from="n" + ), + ) + +wire = bytes(CountedUnframed(n=2, values=[3, 4])) +assert wire == b"\x02\x03\x04" +pkt = CountedUnframed(wire) +assert pkt.n == 2 +assert pkt.values == [3, 4] + += Indefinite ARRAY with count_from uses the dissected discriminator +from scapy.cbor.cborfields import ( + CBORF_ARRAY_INDEFINITE, + CBORF_INTEGER, + CBORF_SEQUENCE_OF, +) +from scapy.cborpacket import CBOR_Packet + +class CountedIndef(CBOR_Packet): + CBOR_root = CBORF_ARRAY_INDEFINITE( + CBORF_INTEGER("n", 0), + CBORF_SEQUENCE_OF( + "values", [], CBORF_INTEGER, count_from="n" + ), + ) + +wire = bytes(CountedIndef(n=2, values=[3, 4])) +pkt = CountedIndef(wire) +assert pkt.n == 2 +assert pkt.values == [3, 4] + += Conditional trailing field follows a false discriminator with optional present +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_CONDITIONAL, + CBORF_INTEGER, + CBORF_optional, +) +from scapy.cborpacket import CBOR_Packet + +class ConditionalOpt(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER("flag", 1), + CBORF_optional(CBORF_INTEGER("optional", None)), + CBORF_CONDITIONAL( + CBORF_INTEGER("last", 8), lambda p: p.flag == 1 + ), + ) + +wire = bytes(ConditionalOpt(flag=0, optional=3)) +pkt = ConditionalOpt(wire) +assert pkt.flag == 0 +assert pkt.optional == 3 + = Optional same-type scalar reserves the sole item for a required tail from scapy.cbor.cborfields import ( CBORF_ARRAY, From d9f8d38da3ab567b4010a68f897d0f7117e58e44 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Thu, 10 Sep 2026 09:31:45 +0200 Subject: [PATCH 52/65] packet: keep FlagsField defaults out of fields for bind overloads Promoting ismutable FlagValue defaults into fields shadowed overloaded_fields, so NBNSHeader built NM_FLAGS=0 and NBNS_am failed. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cborpacket.py | 2 +- scapy/packet.py | 31 ++++++++++++++++++++++++++----- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/scapy/cborpacket.py b/scapy/cborpacket.py index 47e65da138d..a5df420f137 100644 --- a/scapy/cborpacket.py +++ b/scapy/cborpacket.py @@ -82,7 +82,7 @@ def do_init_cached_fields(self, for_dissect_only=False): ) if for_dissect_only: return - # Packet copies ismutable defaults; only re-parent nested packets here. + # Packet isolates list/Packet defaults into fields; re-parent those. for f in self.packetfields: if f.name in self.fields: self.fields[f.name] = f.any2i(self, self.fields[f.name]) diff --git a/scapy/packet.py b/scapy/packet.py index 0c63b811dbb..1d8010e4909 100644 --- a/scapy/packet.py +++ b/scapy/packet.py @@ -116,6 +116,9 @@ class Packet( class_packetfields = {} # type: Dict[Type[Packet], Any] class_default_fields = {} # type: Dict[Type[Packet], Dict[str, Any]] class_default_fields_ref = {} # type: Dict[Type[Packet], List[str]] + # ismutable defaults (e.g. FlagsField) stay in default_fields so bind + # overloads still apply; only list/dict/Packet/RandField go into fields. + class_default_fields_ismutable = {} # type: Dict[Type[Packet], List[str]] class_fieldtype = {} # type: Dict[Type[Packet], Dict[str, AnyField]] # noqa: E501 @classmethod @@ -367,12 +370,25 @@ def do_init_cached_fields(self, for_dissect_only=False): if for_dissect_only: return - # Deepcopy default references + # Deepcopy default references into fields (list/dict/Packet/…) for fname in Packet.class_default_fields_ref[cls_name]: value = self.default_fields[fname] fld = self.fieldtype[fname] self.fields[fname] = fld.do_copy(value) + # Isolate ismutable defaults in a private default_fields dict so + # overloaded_fields from bindings still win on build. + ismutable_names = Packet.class_default_fields_ismutable.get( + cls_name + ) + if ismutable_names: + self.default_fields = dict(default_fields) + for fname in ismutable_names: + fld = self.fieldtype[fname] + self.default_fields[fname] = fld.do_copy( + default_fields[fname] + ) + def prepare_cached_fields(self, flist): # type: (Sequence[AnyField]) -> None """ @@ -387,6 +403,7 @@ def prepare_cached_fields(self, flist): class_default_fields = dict() class_default_fields_ref = list() + class_default_fields_ismutable = list() class_fieldtype = dict() class_packetfields = list() @@ -403,14 +420,18 @@ def prepare_cached_fields(self, flist): if f.holds_packets: class_packetfields.append(f) - # Remember references that need a per-instance copy - if getattr(f, "ismutable", False) or isinstance( - f.default, (list, dict, set, RandField, Packet) - ): + # list/dict/set/Packet/RandField: promote copies into fields + if isinstance(f.default, (list, dict, set, RandField, Packet)): class_default_fields_ref.append(f.name) + # Other ismutable defaults (FlagsField): isolate in default_fields + elif getattr(f, "ismutable", False): + class_default_fields_ismutable.append(f.name) # Apply Packet.class_default_fields_ref[cls_name] = class_default_fields_ref + Packet.class_default_fields_ismutable[cls_name] = ( + class_default_fields_ismutable + ) Packet.class_fieldtype[cls_name] = class_fieldtype Packet.class_packetfields[cls_name] = class_packetfields # Last to avoid racing issues From 4bb7d8977803e5b65d290ec25a278f58d008a5de Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Thu, 10 Sep 2026 09:37:27 +0200 Subject: [PATCH 53/65] cbor: nested encode via bytes(); ANY counted build honors build() Drop the CBOR_Packet counted-build fast path so nested PACKET encode includes post_build/payload, and route ANY through public build() like NULL/UNDEFINED. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborfields.py | 28 ++++++++++++---------------- test/scapy/layers/cbor.uts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index d635f2e414a..5ca6eeb2d4e 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -135,20 +135,9 @@ 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. - 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. + Always goes through ``bytes(val)`` so Packet ``post_build`` / payload are + included, then fully decodes to prove single-item cardinality. """ - from scapy.cborpacket import CBOR_Packet - if isinstance(val, CBOR_Packet): - result = val._cbor_build_counted() - 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) - ) - return result.data data = bytes(val) try: _obj, remaining = CBORcodec_Object.decode_cbor_item(data) @@ -597,12 +586,19 @@ def any2i(self, pkt, x): return x return self.python_to_cbor_object(x) - def _build_counted(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 b"" + return self.i2m(pkt, val) + + def _build_counted(self, pkt): + # type: (CBOR_Packet) -> _CBORBuildResult + data = self.build(pkt) + if not data: return _CBORBuildResult(b"", 0) - return _CBORBuildResult(self.i2m(pkt, val), 1) + return _CBORBuildResult(data, 1) def m2i(self, pkt, s): # type: (CBOR_Packet, bytes) -> Tuple[Any, bytes] diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index 3254cd0886c..3cb8adf4e1c 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -2615,6 +2615,36 @@ try: except CBOR_Encoding_Error: pass += Nested CBORF_PACKET honors child post_build +from scapy.cbor.cborfields import CBORF_PACKET, CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class PostBuildChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) + def post_build(self, pkt, pay): + # Force a distinct single CBOR unsigned integer on the wire. + return b"\x18\x2a" + pay + +class PostBuildParent(CBOR_Packet): + CBOR_root = CBORF_PACKET("child", None, PostBuildChild) + +child = PostBuildChild(value=1) +assert bytes(child) == b"\x18\x2a" +bytes(PostBuildParent(child=child)) == bytes(child) + += CBORF_ANY subclass build() is used by ARRAY counted path +from scapy.cbor.cborfields import CBORF_ANY, CBORF_ARRAY +from scapy.cborpacket import CBOR_Packet + +class ForcedAny(CBORF_ANY): + def build(self, pkt): + return b"\x18\x63" + +class ForcedAnyPkt(CBOR_Packet): + CBOR_root = CBORF_ARRAY(ForcedAny("value", None)) + +bytes(ForcedAnyPkt(value=0)) == b"\x81\x18\x63" + = 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 7400e8696b748f7ddfa237765bc2c9c8a22e5e21 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Thu, 10 Sep 2026 09:39:09 +0200 Subject: [PATCH 54/65] cbor: inherit BYTE_STRING_PACKET; trim absent/budget/float helpers Reuse BYTE_STRING decode for packet wrappers, mark only optionals absent, drop an unreachable budget raise, override _raw_packet_cache_is_valid, and dedupe half-float / float-key paths. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cbor.py | 37 ++++++++++--------------------------- scapy/cbor/cborcodec.py | 12 +++++------- scapy/cbor/cborfields.py | 39 +++++++++------------------------------ scapy/cborpacket.py | 9 +++++---- 4 files changed, 29 insertions(+), 68 deletions(-) diff --git a/scapy/cbor/cbor.py b/scapy/cbor/cbor.py index 7e86f60c4cd..07196d50461 100644 --- a/scapy/cbor/cbor.py +++ b/scapy/cbor/cbor.py @@ -791,6 +791,7 @@ def _cbor_float_key_identity(value, encoded=None): and sign survive Python's NaN canonicalization. """ if encoded is not None: + from scapy.cbor.cborcodec import _cbor_float_from_bits ai, bits = _cbor_float_wire_parts(encoded) if ai == int(CBOR_FloatAI.HALF): sign = (bits >> 15) & 0x1 @@ -798,38 +799,20 @@ def _cbor_float_key_identity(value, encoded=None): 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): + elif ai == int(CBOR_FloatAI.SINGLE): 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) - # 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) + else: + # DOUBLE + sign = (bits >> 63) & 0x1 + exponent = (bits >> 52) & 0x7ff + fraction = bits & ((1 << 52) - 1) + if exponent == 0x7ff and fraction: + return ("nan", sign, fraction) + return _cbor_float_key_identity(_cbor_float_from_bits(ai, bits)) fval = float(value) if math.isnan(fval): diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py index d20a8a65277..c13360e5bbe 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -387,13 +387,11 @@ def _cbor_float_to_half_bits(value): 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(int(CBOR_FloatAI.HALF), sign | half) != preferred: - # Compare absolute then restore sign via copysign on left side - 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 + bits = sign | half + 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 half_exp = exp64 + 15 half_mant = mant64 >> 42 # Reject if discarded mantissa bits are nonzero (not exact). diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 5ca6eeb2d4e..9868266a5ea 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -784,12 +784,13 @@ def randval(self): return RandString(RandNum(0, 1000)) -class CBORF_BYTE_STRING_PACKET(CBORF_field[Packet]): +class CBORF_BYTE_STRING_PACKET(CBORF_BYTE_STRING): """CBOR byte string which wraps another packet field. The inner packet may or may not itself be CBOR or CBOR sequence data. + Inherits definite-length checks and byte-string encode/decode from + :class:`CBORF_BYTE_STRING`. """ - CBOR_tag = CBOR_MajorTypes.BYTE_STRING holds_packets = 1 def __init__(self, @@ -805,8 +806,9 @@ def __init__(self, # 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) + super(CBORF_BYTE_STRING_PACKET, self).__init__( + name, default, definite_only=definite_only + ) def _decode_packet_value(self, pkt, data): # type: (CBOR_Packet, bytes) -> Packet @@ -839,26 +841,8 @@ def any2i(self, 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 != int(CBOR_MajorTypes.BYTE_STRING): - raise CBOR_Type_Mismatch( - "Expected byte string, got major type %d" % major_type) - if length is CBOR_INDEFINITE: - raise CBOR_Decoding_Error( - "Indefinite-length byte string not allowed here") - obj, remain = CBORcodec_BYTE_STRING.dec(s) - if not isinstance(obj, CBOR_BYTE_STRING): - raise CBOR_Type_Mismatch( - "Expected byte string, got %r" % obj) - return self._decode_packet_value(pkt, obj.val), remain - - def encode_value(self, x): - # type: (Any) -> bytes - return CBORcodec_BYTE_STRING.enc(bytes(x)) + data, remain = super(CBORF_BYTE_STRING_PACKET, self).m2i(pkt, s) + return self._decode_packet_value(pkt, data), remain class CBORF_TEXT_STRING(CBORF_field[str]): @@ -1169,12 +1153,9 @@ def _build_children(self, pkt): def _mark_absent(self, pkt, field): # type: (CBOR_Packet, Any) -> None - """Record that an optional/conditional field was not present.""" + """Record that an optional field was not present on the wire.""" if isinstance(field, CBORF_optional): field._field.mark_absent(pkt) - elif isinstance(field, CBORF_CONDITIONAL): - # Condition false or skipped: leave value untouched. - pass def _reject_ambiguous_unbounded_sequences(self): # type: () -> None @@ -1216,8 +1197,6 @@ def _dissect_children_budgeted(self, pkt, s, count): 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 diff --git a/scapy/cborpacket.py b/scapy/cborpacket.py index a5df420f137..827a85ba2f5 100644 --- a/scapy/cborpacket.py +++ b/scapy/cborpacket.py @@ -49,7 +49,7 @@ class CBOR_Packet(Packet, metaclass=CBORPacket_metaclass): CBOR_root = None # type: Optional[Any] - def _raw_cache_is_valid(self): + def _raw_packet_cache_is_valid(self): # type: () -> bool """Return True if ``raw_packet_cache`` still matches nested field state.""" if not super(CBOR_Packet, self)._raw_packet_cache_is_valid(): @@ -66,7 +66,7 @@ def _cbor_build_counted(self): packet merely to recover cardinality. """ from scapy.cbor.cborfields import _CBORBuildResult - if self._raw_cache_is_valid(): + if self._raw_packet_cache_is_valid(): items = getattr(self, "_cbor_raw_cache_items", None) if items is None: items = 1 @@ -118,10 +118,11 @@ def _child_fp(child): or cf.holds_packets or getattr(cf, "ismutable", False) ) and cval is not None: - child_fields[cf.name] = \ + child_fields[cf.name] = ( child._raw_packet_cache_field_value( cf, cval, copy=copy ) + ) else: child_fields[cf.name] = cval else: @@ -139,7 +140,7 @@ def _child_fp(child): def self_build(self): # type: () -> bytes - if self._raw_cache_is_valid(): + if self._raw_packet_cache_is_valid(): return self.raw_packet_cache return self.CBOR_root.build(self) From 3a5d313966b9a54b7326a57c4c7566514a7b94a2 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Thu, 10 Sep 2026 11:15:10 +0200 Subject: [PATCH 55/65] Split/cbor review budgets (#48) * cbor: reserve structural mins until discriminators are ready Suffix budgeting uses structural_min_items so default count_from and conditionals cannot starve leading fields; live mins apply once string count_from sources are in pkt.fields. AI-Assisted: yes (Composer) Co-authored-by: Cursor * cbor: stream unframed SEQUENCE without trailing pre-count Stop after schema fields so counted SEQUENCE_OF leaves valid, bare break, and truncated suffixes unchanged; framed ARRAY budgets stay strict. AI-Assisted: yes (Composer) Co-authored-by: Cursor * cbor: propagate holds_packets from packet-valued item fields ARRAY_OF/SEQUENCE_OF with CBORF_PACKET elements register as packet storage and reuse nested fingerprints while encode/decode still follow item_field versus packet-class selection. AI-Assisted: yes (Composer) Co-authored-by: Cursor * cbor: drop unused packet-level cardinality cache Remove _cbor_build_counted and _cbor_raw_cache_items; nested encode keeps exact wire via Packet raw cache and bytes(child). AI-Assisted: yes (Composer) Co-authored-by: Cursor * cbor: share byte-string wire helpers; restore Packet field typing Extract decode/encode helpers so CBORF_BYTE_STRING_PACKET stays CBORF_field[Packet] while reusing definite-length byte-string handling. AI-Assisted: yes (Composer) Co-authored-by: Cursor * cbor: simplify compound parse budgeting Move suffix reservation onto reserve_min_items, share field dissect helpers, and drop the compound-side ready/budget branching. AI-Assisted: yes (Composer) Co-authored-by: Cursor * cbor: fix flake8 blank lines after budget simplify AI-Assisted: yes (Composer) Co-authored-by: Cursor --------- Co-authored-by: Nils Weiss Co-authored-by: Cursor --- scapy/cbor/cborfields.py | 333 +++++++++++++++++++++++-------------- scapy/cborpacket.py | 36 +--- test/scapy/layers/cbor.uts | 264 +++++++++++++++++++++++++++-- 3 files changed, 464 insertions(+), 169 deletions(-) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 9868266a5ea..97e8a9cbc4d 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -9,8 +9,8 @@ Public leaf/compound hooks follow Scapy/ASN.1 style (``any2i`` / ``i2m`` / ``m2i``, ``build`` / ``dissect``). Compounds additionally use ``_build_counted`` / ``_dissect_counted`` so unframed sequences and array -budgeting can return an item count for raw-cache fidelity; callers outside -this module should prefer ``build`` / ``dissect``. +budgeting can return an item count; callers outside this module should +prefer ``build`` / ``dissect``. """ import copy @@ -204,6 +204,11 @@ def structural_max_items(self, pkt): """Upper bound independent of not-yet-dissected discriminators.""" return self.max_items(pkt) + def reserve_min_items(self, pkt): + # type: (CBOR_Packet) -> int + """Items to reserve for this field while parsing earlier siblings.""" + return self.structural_min_items(pkt) + ########################## # Basic CBOR Field # @@ -735,6 +740,33 @@ def randval(self): return RandNum(-2 ** 64, 2 ** 64 - 1) +def _cbor_decode_byte_string(s, definite_only=False): + # type: (bytes, bool) -> Tuple[bytes, bytes] + """Decode one CBOR byte string item; optionally reject indefinite form.""" + if definite_only: + try: + major_type, length, _rem = CBOR_decode_head(s) + except CBOR_Codec_Decoding_Error as e: + raise CBOR_Decoding_Error(str(e)) + if major_type != int(CBOR_MajorTypes.BYTE_STRING): + raise CBOR_Type_Mismatch( + "Expected byte string, got major type %d" % major_type) + if length is CBOR_INDEFINITE: + raise CBOR_Decoding_Error( + "Indefinite-length byte string not allowed here") + obj, remain = CBORcodec_BYTE_STRING.dec(s) + if not isinstance(obj, CBOR_BYTE_STRING): + raise CBOR_Type_Mismatch( + "Expected byte string, got %r" % obj) + return obj.val, remain + + +def _cbor_encode_byte_string(x): + # type: (Any) -> bytes + """Encode *x* as a definite CBOR byte string item.""" + return CBORcodec_BYTE_STRING.enc(bytes(x)) + + class CBORF_BYTE_STRING(CBORF_field[bytes]): """CBOR byte string field (major type 2).""" CBOR_tag = CBOR_MajorTypes.BYTE_STRING @@ -758,39 +790,24 @@ def any2i(self, pkt, 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 != int(CBOR_MajorTypes.BYTE_STRING): - raise CBOR_Type_Mismatch( - "Expected byte string, got major type %d" % major_type) - if length is CBOR_INDEFINITE: - raise CBOR_Decoding_Error( - "Indefinite-length byte string not allowed here") - obj, remain = CBORcodec_BYTE_STRING.dec(s) - if not isinstance(obj, CBOR_BYTE_STRING): - raise CBOR_Type_Mismatch( - "Expected byte string, got %r" % obj) - return obj.val, remain + return _cbor_decode_byte_string(s, definite_only=self.definite_only) def encode_value(self, x): # type: (Any) -> bytes - return CBORcodec_BYTE_STRING.enc(bytes(x)) + return _cbor_encode_byte_string(x) def randval(self): # type: () -> RandString return RandString(RandNum(0, 1000)) -class CBORF_BYTE_STRING_PACKET(CBORF_BYTE_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. - Inherits definite-length checks and byte-string encode/decode from - :class:`CBORF_BYTE_STRING`. + Shares byte-string wire helpers with :class:`CBORF_BYTE_STRING`. """ + CBOR_tag = CBOR_MajorTypes.BYTE_STRING holds_packets = 1 def __init__(self, @@ -806,9 +823,8 @@ def __init__(self, # any2i() needs these during default normalization in super().__init__. self.pkt_cls = pkt_cls self.cls_cb = cls_cb - super(CBORF_BYTE_STRING_PACKET, self).__init__( - name, default, definite_only=definite_only - ) + self.definite_only = definite_only + super(CBORF_BYTE_STRING_PACKET, self).__init__(name, default) def _decode_packet_value(self, pkt, data): # type: (CBOR_Packet, bytes) -> Packet @@ -841,9 +857,15 @@ def any2i(self, pkt, x): def m2i(self, pkt, s): # type: (CBOR_Packet, bytes) -> Tuple[Packet, bytes] - data, remain = super(CBORF_BYTE_STRING_PACKET, self).m2i(pkt, s) + data, remain = _cbor_decode_byte_string( + s, definite_only=self.definite_only + ) return self._decode_packet_value(pkt, data), remain + def encode_value(self, x): + # type: (Any) -> bytes + return _cbor_encode_byte_string(x) + class CBORF_TEXT_STRING(CBORF_field[str]): """CBOR text string field (major type 3).""" @@ -1151,12 +1173,47 @@ def _build_children(self, pkt): total_items += result.items return b"".join(parts), total_items + def _suffix_reserved(self, pkt, index): + # type: (CBOR_Packet, int) -> int + return sum( + f.reserve_min_items(pkt) for f in self.seq[index + 1:] + ) + def _mark_absent(self, pkt, field): # type: (CBOR_Packet, Any) -> None """Record that an optional field was not present on the wire.""" if isinstance(field, CBORF_optional): field._field.mark_absent(pkt) + def _validate_skipped_optional(self, pkt, field, remaining): + # type: (CBOR_Packet, Any, bytes) -> None + """Reject a present-but-skipped optional that is malformed on the wire.""" + if ( + isinstance(field, CBORF_optional) + and remaining + and field._field.matches_next_item(pkt, remaining) + ): + field._field._parse_value(pkt, remaining) + + def _dissect_field(self, pkt, field, remaining, max_items=None): + # type: (CBOR_Packet, Any, bytes, Optional[int]) -> _CBORParseResult + if isinstance(field, CBORF_optional): + if not remaining or not field._field.matches_next_item( + pkt, remaining + ): + self._mark_absent(pkt, field) + return _CBORParseResult(remaining=remaining, items=0) + result = field._dissect_counted(pkt, remaining) + elif isinstance(field, CBORF_SEQUENCE_OF): + result = field._dissect_counted( + pkt, remaining, max_items=max_items + ) + else: + result = field._dissect_counted(pkt, remaining) + if result.items == 0: + self._mark_absent(pkt, field) + return result + def _reject_ambiguous_unbounded_sequences(self): # type: () -> None for index, field in enumerate(self.seq): @@ -1185,54 +1242,69 @@ def _dissect_children_budgeted(self, pkt, s, count): remaining = s items_left = count for index, field in enumerate(self.seq): - # Live suffix reservation so count_from / conditionals see wire - # discriminators dissected earlier in this pass. - reserved = sum( - f.min_items(pkt) for f in self.seq[index + 1:] - ) + reserved = self._suffix_reserved(pkt, index) available = items_left - reserved needed = field.min_items(pkt) - if available < 0: - raise CBOR_Decoding_Error("CBOR item count mismatch") - if available < needed: + if available < 0 or available < needed: raise CBOR_Decoding_Error("CBOR item count mismatch") if available == 0: - # 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) - ): - # Validate without constructing a throwaway packet. - field._field._parse_value(pkt, remaining) + self._validate_skipped_optional(pkt, field, remaining) self._mark_absent(pkt, field) continue - if isinstance(field, CBORF_SEQUENCE_OF): - 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_counted(pkt, remaining) - else: - result = field._dissect_counted(pkt, remaining) + result = self._dissect_field( + pkt, field, remaining, max_items=available + ) if result.items > items_left: raise CBOR_Decoding_Error( "CBOR field consumed more items than remaining" ) - 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 + def _dissect_children_streamed(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[bytes, int] + """Consume schema fields from *s* without pre-counting trailing bytes.""" + remaining = s + total_items = 0 + for index, field in enumerate(self.seq): + if isinstance(field, CBORF_optional): + suffix_need = self._suffix_reserved(pkt, index) + if ( + not remaining + or not field._field.matches_next_item(pkt, remaining) + ): + self._mark_absent(pkt, field) + continue + if suffix_need > 0: + try: + ahead = cbor_count_items( + remaining, + max_count=suffix_need + 1, + until_break=False, + ) + except CBOR_Codec_Decoding_Error as e: + raise CBOR_Decoding_Error(str(e)) + if ahead <= suffix_need: + if ahead == suffix_need: + self._validate_skipped_optional( + pkt, field, remaining + ) + self._mark_absent(pkt, field) + continue + result = self._dissect_field(pkt, field, remaining) + if ( + isinstance(field, CBORF_SEQUENCE_OF) + and field.count_from is not None + and result.items != field.min_items(pkt) + ): + raise CBOR_Decoding_Error("CBOR item count mismatch") + remaining = result.remaining + total_items += result.items + return remaining, total_items + class CBORF_SEQUENCE(_CBORF_compound): """ @@ -1263,18 +1335,10 @@ def _build_counted(self, pkt): def _dissect_counted(self, pkt, s): # type: (CBOR_Packet, bytes) -> _CBORParseResult - # Count only up to this schema's structural max so trailing CBOR items - # remain for a parent, without freezing count_from on defaults. - try: - item_count = cbor_count_items( - s, - max_count=self.structural_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) + # Stream schema fields only; leave trailing bytes for the parent + # without requiring them to be well-formed CBOR. + remaining, items = self._dissect_children_streamed(pkt, s) + return _CBORParseResult(remaining=remaining, items=items) def min_items(self, pkt): # type: (CBOR_Packet) -> int @@ -1428,7 +1492,11 @@ def __init__(self, self.item_field = pkt_cls("_item", None) # type: ignore else: self.item_field = pkt_cls - self.holds_packets = 0 + # Packet-valued element fields must register as packet storage + # even though decode/encode still go through item_field. + self.holds_packets = 1 if getattr( + self.item_field, "holds_packets", False + ) else 0 else: self.cls = self._require_packet_cls(pkt_cls) self.holds_packets = 1 @@ -1436,7 +1504,11 @@ def __init__(self, def cache_fingerprint(self, x): # type: (Any) -> Any - """Compose item fingerprints when the element field provides them.""" + """Compose item fingerprints when the element field provides them. + + Packet-valued collections return ``None`` so the parent packet uses + nested ``_raw_packet_cache_field_value`` composition. + """ if self.holds_packets or self.item_field is None or x is None: return None item_fp = getattr(self.item_field, "cache_fingerprint", None) @@ -1477,63 +1549,63 @@ 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] + if self.item_field is not None: + return [self.item_field.any2i(pkt, item) for item in x] + items = list(x) + for item in items: + _cbor_attach_parent(pkt, item) + return items def _decode_element(self, pkt, s, values=None): # type: (CBOR_Packet, bytes, Optional[List[Any]]) -> Tuple[Any, bytes] - if self.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 self.item_field is not None: + result = self.item_field._parse_value(pkt, s) + if result.items != 1: + raise CBOR_Decoding_Error( + "%s element must consume exactly one item" + % self.__class__.__name__ ) - if pkt_cls is CBOR_NO_ITEM or pkt_cls is None: - return CBOR_NO_ITEM, s - pkt_cls = self._require_packet_cls(pkt_cls) - item_bytes, remaining = cbor_item_span(s) - try: - child = pkt_cls(item_bytes, _parent=pkt) # type: ignore - except CBOR_Decoding_Error: - raise - except Exception as exc: - if config.conf.debug_dissector: - raise - raise CBOR_Decoding_Error(str(exc)) - return child, remaining - result = self.item_field._parse_value(pkt, s) - if result.items != 1: - raise CBOR_Decoding_Error( - "%s element must consume exactly one item" - % self.__class__.__name__ + return result.value, result.remaining + pkt_cls = self.cls + if self.next_cls_cb is not None: + values = values if values is not None else [] + pkt_cls = self.next_cls_cb( + pkt, + values, + values[-1] if values else None, + s, ) - return result.value, result.remaining + if pkt_cls is CBOR_NO_ITEM or pkt_cls is None: + return CBOR_NO_ITEM, s + pkt_cls = self._require_packet_cls(pkt_cls) + item_bytes, remaining = cbor_item_span(s) + try: + child = pkt_cls(item_bytes, _parent=pkt) # type: ignore + except CBOR_Decoding_Error: + raise + except Exception as exc: + if config.conf.debug_dissector: + raise + raise CBOR_Decoding_Error(str(exc)) + return child, remaining def _encode_element(self, pkt, item): # type: (CBOR_Packet, Any) -> bytes - if self.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 + if self.item_field is not None: + result = self.item_field._build_value(pkt, item) + if result.items != 1: + raise CBOR_Encoding_Error( + "%s element must emit exactly one item" + % self.__class__.__name__ + ) + return result.data + return _encode_exactly_one_cbor_item( + item, context="%s element" % self.__class__.__name__ + ) def i2repr(self, pkt, x): # type: (CBOR_Packet, Any) -> str - if self.holds_packets: + if self.item_field is None: return repr(x) if x is None: return self._empty_repr @@ -1603,7 +1675,7 @@ def _decode_items(self, pkt, data, max_items=None): if callable(self.count_from): max_items = int(self.count_from(pkt)) else: - max_items = int(pkt.getfieldval(self.count_from)) + max_items = self.min_items(pkt) while remaining and not cbor_is_break(remaining): if max_items is not None and consumed >= max_items: break @@ -1646,11 +1718,17 @@ def _build_counted(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 + if self.count_from is None or pkt is None: + return 0 + if callable(self.count_from): + return int(self.count_from(pkt)) + # Only dissected values — never fall back to field defaults. + if self.count_from not in pkt.fields: + return 0 + val = pkt.fields[self.count_from] + if val is None or val is CBOR_ABSENT: + return 0 + return int(val) def max_items(self, pkt): # type: (CBOR_Packet) -> int @@ -1666,6 +1744,13 @@ def structural_max_items(self, pkt): # type: (CBOR_Packet) -> int return self._list_limit() + def reserve_min_items(self, pkt): + # type: (CBOR_Packet) -> int + # Callables have no dependency tracking; reserve nothing until current. + if callable(self.count_from): + return 0 + return self.min_items(pkt) + class CBORF_ARRAY_OF(_CBORF_HOMOGENEOUS): """ diff --git a/scapy/cborpacket.py b/scapy/cborpacket.py index 827a85ba2f5..b0d936a3262 100644 --- a/scapy/cborpacket.py +++ b/scapy/cborpacket.py @@ -7,7 +7,7 @@ Packet holding data encoded in Concise Binary Object Representation (CBOR). Modelled after scapy/asn1packet.py, with CBOR-specific raw-cache integration -for sentinels (``CBOR_ABSENT``), mutable ANY values, and nested item counts. +for sentinels (``CBOR_ABSENT``) and mutable ANY values. """ from scapy.base_classes import Packet_metaclass @@ -43,38 +43,11 @@ class CBOR_Packet(Packet, metaclass=CBORPacket_metaclass): Field flags (``islist`` / ``ismutable`` / ``holds_packets``) drive 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. + re-parents nested packet defaults for exact-wire rebuilds. """ CBOR_root = None # type: Optional[Any] - def _raw_packet_cache_is_valid(self): - # type: () -> bool - """Return True if ``raw_packet_cache`` still matches nested field state.""" - if not super(CBOR_Packet, self)._raw_packet_cache_is_valid(): - self._cbor_raw_cache_items = None # type: ignore[attr-defined] - return False - return True - - def _cbor_build_counted(self): - # type: () -> Any - """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 - if self._raw_packet_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_counted(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( @@ -150,7 +123,6 @@ def do_dissect(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] self.raw_packet_cache_fields = {} for f in self.fields_desc: if f.name not in self.fields: @@ -177,10 +149,6 @@ def copy(self): ``parent`` for ownership, so reattach after the clone is built. """ clone = super(CBOR_Packet, self).copy() - if hasattr(self, "_cbor_raw_cache_items"): - clone._cbor_raw_cache_items = ( # type: ignore[attr-defined] - self._cbor_raw_cache_items - ) for f in clone.fields_desc: if f.holds_packets and f.name in clone.fields: clone.fields[f.name] = f.any2i(clone, clone.fields[f.name]) diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index 3cb8adf4e1c..a014380811e 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -712,6 +712,77 @@ pkt = AnyArrayFloatPkt(bytes.fromhex("81f90000")) pkt.values[0].val = -0.0 assert bytes(pkt) == bytes.fromhex("81f98000") += Packet-valued ARRAY_OF and SEQUENCE_OF fingerprint copy and wire cache +from scapy.cbor.cborfields import ( + CBORF_ANY, + CBORF_ARRAY_OF, + CBORF_PACKET, + CBORF_SEQUENCE, + CBORF_SEQUENCE_OF, +) +from scapy.cbor.cbor import CBOR_FLOAT, CBOR_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class NestedAnyChild(CBOR_Packet): + CBOR_root = CBORF_ANY("value", None) + +# Packet-class constructor +class ArrayOfChildCls(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF("items", [], NestedAnyChild) + +# Packet-field-instance constructor +class ArrayOfChildFld(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF( + "items", [], CBORF_PACKET("item", None, NestedAnyChild) + ) + +class SeqOfChildCls(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_SEQUENCE_OF("items", [], NestedAnyChild), + ) + +class SeqOfChildFld(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_SEQUENCE_OF( + "items", [], CBORF_PACKET("item", None, NestedAnyChild) + ), + ) + +wire = bytes.fromhex("81f90000") +for cls in (ArrayOfChildCls, ArrayOfChildFld): + assert cls.CBOR_root.holds_packets == 1 + pkt = cls(wire) + assert bytes(pkt) == wire + pkt.items[0].value.val = -0.0 + assert bytes(pkt) == bytes.fromhex("81f98000") + +for cls in (ArrayOfChildCls, ArrayOfChildFld): + pkt = cls(wire) + pkt.items[0].value = CBOR_FLOAT(0.0) + assert getattr(pkt.items[0].value, "_encoded", None) is None + assert bytes(pkt) == bytes.fromhex("81f90000") + +for cls in (ArrayOfChildCls, ArrayOfChildFld): + pkt = cls(bytes.fromhex("81820102")) + pkt.items[0].value.val.append(CBOR_UNSIGNED_INTEGER(3)) + assert bytes(pkt) == bytes.fromhex("8183010203") + +for cls in (ArrayOfChildCls, ArrayOfChildFld): + pkt = cls(wire) + clone = pkt.copy() + assert clone.items[0].parent is clone + clone.items[0].value.val = -0.0 + assert bytes(pkt) == wire + assert bytes(clone) == bytes.fromhex("81f98000") + +seq_wire = bytes.fromhex("f90000") +for cls in (SeqOfChildCls, SeqOfChildFld): + assert cls.CBOR_root.seq[0].holds_packets == 1 + pkt = cls(seq_wire) + assert bytes(pkt) == seq_wire + pkt.items[0].value.val = -0.0 + assert bytes(pkt) == bytes.fromhex("f98000") + = Unknown map float mutation invalidates raw cache from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER, CBORF_optional from scapy.cborpacket import CBOR_Packet @@ -1137,6 +1208,153 @@ pkt = OptThenReq(b"\x81\x07") assert pkt.opt is CBOR_ABSENT assert pkt.req == 7 += Count default 5 with wire count 1 decodes on ARRAY SEQUENCE and indefinite +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_ARRAY_INDEFINITE, + CBORF_INTEGER, + CBORF_SEQUENCE, + CBORF_SEQUENCE_OF, +) +from scapy.cborpacket import CBOR_Packet + +class CountDef5Array(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER("n", 5), + CBORF_SEQUENCE_OF("values", [], CBORF_INTEGER, count_from="n"), + ) + +class CountDef5Seq(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_INTEGER("n", 5), + CBORF_SEQUENCE_OF("values", [], CBORF_INTEGER, count_from="n"), + ) + +class CountDef5Indef(CBOR_Packet): + CBOR_root = CBORF_ARRAY_INDEFINITE( + CBORF_INTEGER("n", 5), + CBORF_SEQUENCE_OF("values", [], CBORF_INTEGER, count_from="n"), + ) + +pkt = CountDef5Array(b"\x82\x01\x0a") +assert pkt.n == 1 and pkt.values == [10] +pkt = CountDef5Seq(b"\x01\x0a") +assert pkt.n == 1 and pkt.values == [10] +pkt = CountDef5Indef(b"\x9f\x01\x0a\xff") +assert pkt.n == 1 and pkt.values == [10] + += Count default None does not evaluate int(None) before the discriminator +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_ARRAY_INDEFINITE, + CBORF_INTEGER, + CBORF_SEQUENCE, + CBORF_SEQUENCE_OF, +) +from scapy.cborpacket import CBOR_Packet + +class CountDefNoneArray(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER("n", None), + CBORF_SEQUENCE_OF("values", [], CBORF_INTEGER, count_from="n"), + ) + +class CountDefNoneSeq(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_INTEGER("n", None), + CBORF_SEQUENCE_OF("values", [], CBORF_INTEGER, count_from="n"), + ) + +class CountDefNoneIndef(CBOR_Packet): + CBOR_root = CBORF_ARRAY_INDEFINITE( + CBORF_INTEGER("n", None), + CBORF_SEQUENCE_OF("values", [], CBORF_INTEGER, count_from="n"), + ) + +pkt = CountDefNoneArray(b"\x83\x02\x0a\x0b") +assert pkt.n == 2 and pkt.values == [10, 11] +pkt = CountDefNoneSeq(b"\x02\x0a\x0b") +assert pkt.n == 2 and pkt.values == [10, 11] +pkt = CountDefNoneIndef(b"\x9f\x02\x0a\x0b\xff") +assert pkt.n == 2 and pkt.values == [10, 11] + += Conditional disabled by default enables on wire with present value +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_ARRAY_INDEFINITE, + CBORF_CONDITIONAL, + CBORF_INTEGER, + CBORF_SEQUENCE, +) +from scapy.cborpacket import CBOR_Packet + +class CondOffArray(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER("flag", 0), + CBORF_CONDITIONAL(CBORF_INTEGER("last", 8), lambda p: p.flag == 1), + ) + +class CondOffSeq(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_INTEGER("flag", 0), + CBORF_CONDITIONAL(CBORF_INTEGER("last", 8), lambda p: p.flag == 1), + ) + +class CondOffIndef(CBOR_Packet): + CBOR_root = CBORF_ARRAY_INDEFINITE( + CBORF_INTEGER("flag", 0), + CBORF_CONDITIONAL(CBORF_INTEGER("last", 8), lambda p: p.flag == 1), + ) + +pkt = CondOffArray(b"\x82\x01\x08") +assert pkt.flag == 1 and pkt.last == 8 +pkt = CondOffSeq(b"\x01\x08") +assert pkt.flag == 1 and pkt.last == 8 +pkt = CondOffIndef(b"\x9f\x01\x08\xff") +assert pkt.flag == 1 and pkt.last == 8 + += Optional before count_from collection reserves the collection items +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_INTEGER, + CBORF_SEQUENCE_OF, + CBORF_optional, + CBOR_ABSENT, +) +from scapy.cborpacket import CBOR_Packet + +class OptBeforeCounted(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER("n", 0), + CBORF_optional(CBORF_INTEGER("opt", None)), + CBORF_SEQUENCE_OF("values", [], CBORF_INTEGER, count_from="n"), + ) + +pkt = OptBeforeCounted(b"\x83\x02\x0a\x0b") +assert pkt.n == 2 +assert pkt.opt is CBOR_ABSENT +assert pkt.values == [10, 11] + += Definite ARRAY rejects item count inconsistent with schema +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_INTEGER, + CBOR_Decoding_Error, +) +from scapy.cborpacket import CBOR_Packet + +class TwoIntsArray(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER("a", 0), + CBORF_INTEGER("b", 0), + ) + +try: + TwoIntsArray(b"\x83\x01\x02\x03") + assert False, "inconsistent definite array count must raise" +except CBOR_Decoding_Error: + pass + = Indefinite array rejects nonterminal unbounded SEQUENCE_OF from scapy.cbor.cborfields import ( CBORF_ARRAY_INDEFINITE, @@ -2086,7 +2304,7 @@ except ValueError: else: raise AssertionError("ambiguous separated unbounded sequences were accepted") -+ Finding 8 - Nested _cbor_build_counted must preserve a valid child raw cache ++ Finding 8 - Nested rebuild 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 @@ -2110,7 +2328,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_counted().data == bytes(pkt.child) +assert bytes(pkt.child) == b"\x18\x01" = Parent rebuild preserves an untouched child encoded as an indefinite array from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_PACKET, CBORF_UNSIGNED_INTEGER @@ -2547,9 +2765,7 @@ assert cbor_find_non_deterministic( ) == [] -+ Cache item counts and packet-field cardinality - -+ Cached unframed packets preserve exact bytes and item counts ++ Cached unframed packets preserve exact bytes = Unframed SEQUENCE cache returns exact bytes without rebuild from scapy.cbor.cborfields import ( @@ -2570,11 +2786,7 @@ class SeqChild(CBOR_Packet): 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_counted() -assert result.data == overlong -assert result.items == 2 -assert result.data == bytes(child) +assert bytes(child) == overlong # 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 @@ -3437,6 +3649,33 @@ assert isinstance(pkt.payload, Raw) or pkt.original.endswith(b"\x03") remain = TwoInts.CBOR_root._dissect_counted(TwoInts(), b"\x01\x02\x03").remaining assert remain == b"\x03" += Counted unframed SEQUENCE leaves valid invalid and truncated trailing bytes +from scapy.cbor.cborfields import ( + CBORF_INTEGER, + CBORF_SEQUENCE, + CBORF_SEQUENCE_OF, +) +from scapy.cborpacket import CBOR_Packet +from scapy.packet import Raw + +class CountedSequence(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_INTEGER("n", 0), + CBORF_SEQUENCE_OF( + "values", [], CBORF_INTEGER, count_from="n" + ), + ) + +for suffix in (b"\x04", b"\xff", b"\x18"): + wire = b"\x01\x03" + suffix + remain = CountedSequence.CBOR_root._dissect_counted( + CountedSequence(), wire + ).remaining + assert remain == suffix + pkt = CountedSequence(wire) + assert pkt.n == 1 and pkt.values == [3] + assert isinstance(pkt.payload, Raw) and pkt.payload.load == suffix + = CBORF_SEMANTIC_TAG rejects the wrong tag number from scapy.cbor.cborfields import ( CBORF_SEMANTIC_TAG, CBORF_INTEGER, CBOR_Type_Mismatch, @@ -3728,7 +3967,10 @@ try: assert False, "SEQUENCE accepted over-nested input" except Exception as err: assert not isinstance(err, RecursionError) - assert "nesting" in str(err).lower() + # Streaming SEQUENCE does not pre-count the buffer; the first item is + # rejected as the wrong major type rather than a nesting pre-scan error. + msg = str(err).lower() + assert "nesting" in msg or "major type" in msg # Indefinite array pre-scan uses the same depth-limited skip path. indef_too_deep = b"\x9f" + too_deep + b"\xff" From a7e7faae025627ee4a9780c59c8520e3d92dbd61 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Thu, 10 Sep 2026 11:33:06 +0200 Subject: [PATCH 56/65] cbor: isolate nested TypeError test from debug_dissector pollution regression.uts leaves conf.debug_dissector True, so the nested constructor test must pin False/True explicitly like BYTE_STRING_PACKET. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- test/scapy/layers/cbor.uts | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index a014380811e..835f81ff8e8 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -3337,6 +3337,7 @@ assert BrokenNextClsCalls == [4], BrokenNextClsCalls = Nested packet construction is not retried when the child constructor raises TypeError +from scapy.config import conf from scapy.cbor.cbor import CBOR_Decoding_Error from scapy.cbor.cborfields import CBORF_PACKET, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet @@ -3351,15 +3352,29 @@ class TypeErrorChild(CBOR_Packet): class TypeErrorParent(CBOR_Packet): CBOR_root = CBORF_PACKET("child", None, TypeErrorChild) -TypeErrorChild.init_calls[:] = [] +_old_dbg = conf.debug_dissector 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") + conf.debug_dissector = False + TypeErrorChild.init_calls[:] = [] + try: + TypeErrorParent(b"\x01") + except CBOR_Decoding_Error as exc: + assert "intentional nested packet failure" in str(exc) + else: + raise AssertionError("nested packet TypeError was unexpectedly swallowed") + assert TypeErrorChild.init_calls == [True], TypeErrorChild.init_calls -assert TypeErrorChild.init_calls == [True], TypeErrorChild.init_calls + conf.debug_dissector = True + TypeErrorChild.init_calls[:] = [] + try: + TypeErrorParent(b"\x01") + except TypeError as exc: + assert "intentional nested packet failure" in str(exc) + else: + raise AssertionError("TypeError was hidden despite debug_dissector") + assert TypeErrorChild.init_calls == [True], TypeErrorChild.init_calls +finally: + conf.debug_dissector = _old_dbg + fixed-map unknown member preservation From 8f5cb02e56b7bb8c3de8b07b73957f25bf7b66c2 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Thu, 10 Sep 2026 12:50:15 +0200 Subject: [PATCH 57/65] cbor: terminal SEQUENCE_OF and trim review abstractions Drop count_from, shrink cardinality to structural_min, isolate ismutable defaults on CBOR_Packet, and fold encode_value into i2m. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborfields.py | 152 ++++++++----------------- scapy/cborpacket.py | 15 +++ scapy/packet.py | 23 ---- test/fields.uts | 13 --- test/scapy/layers/cbor.uts | 226 ++++--------------------------------- 5 files changed, 83 insertions(+), 346 deletions(-) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 97e8a9cbc4d..898e2fb2f94 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -204,11 +204,6 @@ def structural_max_items(self, pkt): """Upper bound independent of not-yet-dissected discriminators.""" return self.max_items(pkt) - def reserve_min_items(self, pkt): - # type: (CBOR_Packet) -> int - """Items to reserve for this field while parsing earlier siblings.""" - return self.structural_min_items(pkt) - ########################## # Basic CBOR Field # @@ -259,16 +254,6 @@ def m2i(self, pkt, s): raise NotImplementedError( "Subclasses must implement m2i for %s" % type(self)) - def encode_value(self, x): - # type: (Any) -> bytes - """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).""" @@ -289,7 +274,13 @@ 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_counted(). - return self.encode_value(x) + return self._encode_leaf(x) + + def _encode_leaf(self, x): + # type: (Any) -> bytes + """Encode a native Python value to CBOR bytes (leaf fields only).""" + raise NotImplementedError( + "Subclasses must implement _encode_leaf for %s" % type(self)) @staticmethod def _object_to_python(obj): @@ -340,7 +331,7 @@ 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) + pkt.setfieldval(self.name, val) return remain def _build_counted(self, pkt): @@ -383,22 +374,18 @@ def do_copy(self, x): pass return copy.deepcopy(x) - def set_val(self, pkt, val): - # type: (CBOR_Packet, Any) -> None - 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 mark_absent(self, pkt): # type: (CBOR_Packet) -> None - """Record that this field was not present on the wire.""" - self.set_val(pkt, CBOR_ABSENT) + """Record that this field was not present on the wire. + + Assign ``CBOR_ABSENT`` without ``any2i`` so integer leaves that + reject non-int values still accept the presence sentinel. + """ + pkt.fields[self.name] = CBOR_ABSENT + pkt.explicit = 0 + pkt.raw_packet_cache = None + pkt.raw_packet_cache_fields = None + pkt.wirelen = None def is_empty(self, pkt): # type: (CBOR_Packet) -> bool @@ -609,7 +596,7 @@ def m2i(self, pkt, s): # type: (CBOR_Packet, bytes) -> Tuple[Any, bytes] return CBORcodec_Object.decode_cbor_item(s) - def encode_value(self, x): + def _encode_leaf(self, x): # type: (Any) -> bytes if x is CBOR_ABSENT: return b"" @@ -644,7 +631,7 @@ def m2i(self, pkt, s): "Expected unsigned integer, got %r" % obj) return obj.val, remain - def encode_value(self, x): + def _encode_leaf(self, x): # type: (Any) -> bytes return CBORcodec_UNSIGNED_INTEGER.enc(int(x)) @@ -677,7 +664,7 @@ def m2i(self, pkt, s): "Expected negative integer, got %r" % obj) return obj.val, remain - def encode_value(self, x): + def _encode_leaf(self, x): # type: (Any) -> bytes return CBORcodec_NEGATIVE_INTEGER.enc(int(x)) @@ -728,7 +715,7 @@ def m2i(self, pkt, s): raise CBOR_Type_Mismatch( "Expected integer (major type 0 or 1), got %d" % major_type) - def encode_value(self, x): + def _encode_leaf(self, x): # type: (Any) -> bytes i = int(x) if i >= 0: @@ -792,7 +779,7 @@ def m2i(self, pkt, s): # type: (CBOR_Packet, bytes) -> Tuple[bytes, bytes] return _cbor_decode_byte_string(s, definite_only=self.definite_only) - def encode_value(self, x): + def _encode_leaf(self, x): # type: (Any) -> bytes return _cbor_encode_byte_string(x) @@ -862,7 +849,7 @@ def m2i(self, pkt, s): ) return self._decode_packet_value(pkt, data), remain - def encode_value(self, x): + def _encode_leaf(self, x): # type: (Any) -> bytes return _cbor_encode_byte_string(x) @@ -893,7 +880,7 @@ def m2i(self, pkt, s): "Expected text string, got %r" % obj) return obj.val, remain - def encode_value(self, x): + def _encode_leaf(self, x): # type: (Any) -> bytes return CBORcodec_TEXT_STRING.enc(str(x)) @@ -939,7 +926,7 @@ def m2i(self, pkt, s): "Expected boolean (CBOR_FALSE or CBOR_TRUE), got %r" % obj) return obj.val, remain - def encode_value(self, x): + def _encode_leaf(self, x): # type: (Any) -> bytes return CBORcodec_SIMPLE_AND_FLOAT.enc(bool(x)) @@ -981,7 +968,7 @@ def m2i(self, pkt, s): "Expected null, got %r" % obj) return None, remain - def encode_value(self, x): + def _encode_leaf(self, x): # type: (Any) -> bytes return CBOR_NULL().enc() @@ -989,7 +976,7 @@ def build(self, pkt): # type: (CBOR_Packet) -> bytes if pkt.getfieldval(self.name) is CBOR_ABSENT: return b"" - return self.encode_value(None) + return self._encode_leaf(None) def _build_counted(self, pkt): # type: (CBOR_Packet) -> _CBORBuildResult @@ -1036,7 +1023,7 @@ def m2i(self, pkt, s): "Expected undefined, got %r" % obj) return None, remain - def encode_value(self, x): + def _encode_leaf(self, x): # type: (Any) -> bytes return CBOR_UNDEFINED().enc() @@ -1044,7 +1031,7 @@ def build(self, pkt): # type: (CBOR_Packet) -> bytes if pkt.getfieldval(self.name) is CBOR_ABSENT: return b"" - return self.encode_value(None) + return self._encode_leaf(None) def _build_counted(self, pkt): # type: (CBOR_Packet) -> _CBORBuildResult @@ -1101,7 +1088,7 @@ def m2i(self, pkt, s): "Expected float, got %r" % obj) return float(obj.val), remain - def encode_value(self, x): + def _encode_leaf(self, x): # type: (Any) -> bytes if isinstance(x, CBOR_FLOAT): return CBORcodec_SIMPLE_AND_FLOAT.enc(float(x.val)) @@ -1176,7 +1163,7 @@ def _build_children(self, pkt): def _suffix_reserved(self, pkt, index): # type: (CBOR_Packet, int) -> int return sum( - f.reserve_min_items(pkt) for f in self.seq[index + 1:] + f.structural_min_items(pkt) for f in self.seq[index + 1:] ) def _mark_absent(self, pkt, field): @@ -1225,16 +1212,13 @@ def _reject_ambiguous_unbounded_sequences(self): inner = inner.fld else: break - if not ( - isinstance(inner, CBORF_SEQUENCE_OF) - and getattr(inner, "is_unbounded", False) - ): + if not isinstance(inner, CBORF_SEQUENCE_OF): continue - # Unbounded SEQUENCE_OF must be the final schema field. + # SEQUENCE_OF is always terminal (consumes the remainder). if index != len(self.seq) - 1: raise ValueError( - "Unbounded CBORF_SEQUENCE_OF must be the last field " - "in the sequence (or provide count_from=)" + "CBORF_SEQUENCE_OF must be the last field " + "in the sequence" ) def _dissect_children_budgeted(self, pkt, s, count): @@ -1295,12 +1279,6 @@ def _dissect_children_streamed(self, pkt, s): self._mark_absent(pkt, field) continue result = self._dissect_field(pkt, field, remaining) - if ( - isinstance(field, CBORF_SEQUENCE_OF) - and field.count_from is not None - and result.items != field.min_items(pkt) - ): - raise CBOR_Decoding_Error("CBOR item count mismatch") remaining = result.remaining total_items += result.items return remaining, total_items @@ -1633,9 +1611,10 @@ class CBORF_SEQUENCE_OF(_CBORF_HOMOGENEOUS): :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``). + ``SEQUENCE_OF`` must be the last field in its enclosing positional + schema (``CBORF_SEQUENCE`` / ``CBORF_ARRAY``). It consumes the + remainder until break or exhaustion. Use ``max_count`` to cap + decoding (defaults to ``conf.max_list_count``). """ CBOR_tag = None _empty_repr = "()" @@ -1647,11 +1626,9 @@ 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.count_from = count_from super(CBORF_SEQUENCE_OF, self).__init__( name, default, @@ -1660,22 +1637,12 @@ def __init__(self, max_count=max_count, ) - @property - def is_unbounded(self): - # type: () -> bool - return self.count_from is None - 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 - if self.count_from is not None: - if callable(self.count_from): - max_items = int(self.count_from(pkt)) - else: - max_items = self.min_items(pkt) while remaining and not cbor_is_break(remaining): if max_items is not None and consumed >= max_items: break @@ -1704,7 +1671,7 @@ def _dissect_counted(self, pkt, s, max_items=None): values, remaining, consumed = self._decode_items( pkt, s, max_items=max_items ) - self.set_val(pkt, values) + pkt.setfieldval(self.name, values) return _CBORParseResult(remaining=remaining, items=consumed) def _build_counted(self, pkt): @@ -1717,40 +1684,13 @@ def _build_counted(self, pkt): return _CBORBuildResult(b"".join(parts), len(val)) def min_items(self, pkt): - # type: (CBOR_Packet) -> int - if self.count_from is None or pkt is None: - return 0 - if callable(self.count_from): - return int(self.count_from(pkt)) - # Only dissected values — never fall back to field defaults. - if self.count_from not in pkt.fields: - return 0 - val = pkt.fields[self.count_from] - if val is None or val is CBOR_ABSENT: - return 0 - return int(val) - - def max_items(self, pkt): - # type: (CBOR_Packet) -> int - if self.count_from is not None and pkt is not None: - return self.min_items(pkt) - return self._list_limit() - - def structural_min_items(self, pkt): # type: (CBOR_Packet) -> int return 0 - def structural_max_items(self, pkt): + def max_items(self, pkt): # type: (CBOR_Packet) -> int return self._list_limit() - def reserve_min_items(self, pkt): - # type: (CBOR_Packet) -> int - # Callables have no dependency tracking; reserve nothing until current. - if callable(self.count_from): - return 0 - return self.min_items(pkt) - class CBORF_ARRAY_OF(_CBORF_HOMOGENEOUS): """ @@ -1857,7 +1797,7 @@ def is_empty(self, pkt): # type: (CBOR_Packet) -> bool return not pkt.getfieldval(self.name) - def encode_value(self, x): + def _encode_leaf(self, x): # type: (Any) -> bytes raise CBOR_Encoding_Error( "_CBORF_MAP_UNKNOWN is not encoded as a standalone CBOR item" @@ -2093,7 +2033,7 @@ def _dissect_value_bytes(fld, val_bytes): raise CBOR_Decoding_Error( "Required map field %r is missing" % fld.name ) - self._unknown_field.set_val(pkt, unknown_pairs) + pkt.setfieldval(self._unknown_field.name, unknown_pairs) return _CBORParseResult(remaining=remaining, items=1) @@ -2261,10 +2201,6 @@ def max_items(self, pkt): # type: (CBOR_Packet) -> int return self._field.max_items(pkt) - def structural_min_items(self, pkt): - # type: (CBOR_Packet) -> int - return 0 - def structural_max_items(self, pkt): # type: (CBOR_Packet) -> int return self._field.structural_max_items(pkt) diff --git a/scapy/cborpacket.py b/scapy/cborpacket.py index b0d936a3262..a5e3305defa 100644 --- a/scapy/cborpacket.py +++ b/scapy/cborpacket.py @@ -59,6 +59,21 @@ def do_init_cached_fields(self, for_dissect_only=False): for f in self.packetfields: if f.name in self.fields: self.fields[f.name] = f.any2i(self, self.fields[f.name]) + # Isolate CBOR ismutable defaults (e.g. CBORF_ANY objects) without + # promoting into fields (bind overloads stay Packet-global). + need = [ + f.name for f in self.fields_desc + if getattr(f, "ismutable", False) + and f.name in self.default_fields + and f.name not in self.fields + ] + if need: + self.default_fields = dict(self.default_fields) + for name in need: + fld = self.fieldtype[name] + self.default_fields[name] = fld.do_copy( + self.default_fields[name] + ) def _raw_packet_cache_field_value(self, fld, val, copy=False): # type: (Any, Any, bool) -> Optional[Any] diff --git a/scapy/packet.py b/scapy/packet.py index 1d8010e4909..f5983ac1c99 100644 --- a/scapy/packet.py +++ b/scapy/packet.py @@ -116,9 +116,6 @@ class Packet( class_packetfields = {} # type: Dict[Type[Packet], Any] class_default_fields = {} # type: Dict[Type[Packet], Dict[str, Any]] class_default_fields_ref = {} # type: Dict[Type[Packet], List[str]] - # ismutable defaults (e.g. FlagsField) stay in default_fields so bind - # overloads still apply; only list/dict/Packet/RandField go into fields. - class_default_fields_ismutable = {} # type: Dict[Type[Packet], List[str]] class_fieldtype = {} # type: Dict[Type[Packet], Dict[str, AnyField]] # noqa: E501 @classmethod @@ -376,19 +373,6 @@ def do_init_cached_fields(self, for_dissect_only=False): fld = self.fieldtype[fname] self.fields[fname] = fld.do_copy(value) - # Isolate ismutable defaults in a private default_fields dict so - # overloaded_fields from bindings still win on build. - ismutable_names = Packet.class_default_fields_ismutable.get( - cls_name - ) - if ismutable_names: - self.default_fields = dict(default_fields) - for fname in ismutable_names: - fld = self.fieldtype[fname] - self.default_fields[fname] = fld.do_copy( - default_fields[fname] - ) - def prepare_cached_fields(self, flist): # type: (Sequence[AnyField]) -> None """ @@ -403,7 +387,6 @@ def prepare_cached_fields(self, flist): class_default_fields = dict() class_default_fields_ref = list() - class_default_fields_ismutable = list() class_fieldtype = dict() class_packetfields = list() @@ -423,15 +406,9 @@ def prepare_cached_fields(self, flist): # list/dict/set/Packet/RandField: promote copies into fields if isinstance(f.default, (list, dict, set, RandField, Packet)): class_default_fields_ref.append(f.name) - # Other ismutable defaults (FlagsField): isolate in default_fields - elif getattr(f, "ismutable", False): - class_default_fields_ismutable.append(f.name) # Apply Packet.class_default_fields_ref[cls_name] = class_default_fields_ref - Packet.class_default_fields_ismutable[cls_name] = ( - class_default_fields_ismutable - ) Packet.class_fieldtype[cls_name] = class_fieldtype Packet.class_packetfields[cls_name] = class_packetfields # Last to avoid racing issues diff --git a/test/fields.uts b/test/fields.uts index 3d510036741..e2d1132d414 100644 --- a/test/fields.uts +++ b/test/fields.uts @@ -1704,19 +1704,6 @@ with warnings.catch_warnings(record=True) as w: ######## + FlagsField -= ismutable FlagsField defaults are isolated between packet instances - -class IsolatedFlagsPkt(Packet): - fields_desc = [ - FlagsField("flags", 0, 8, ["A", "B", "C"]) - ] - -a = IsolatedFlagsPkt() -b = IsolatedFlagsPkt() -a.flags.A = True -assert int(a.flags) == 1 -assert int(b.flags) == 0 - = Test Flags Field Iterator class FlagsTest(Packet): diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index 835f81ff8e8..61b8d65d170 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -1077,94 +1077,38 @@ 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 += Optional before trailing SEQUENCE_OF does not invent a reserved count from scapy.cbor.cborfields import ( CBORF_ARRAY, + CBORF_BOOLEAN, 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, - CBORF_SEQUENCE_OF, - CBORF_UNSIGNED_INTEGER, + CBOR_ABSENT, ) from scapy.cborpacket import CBOR_Packet -class CountedSeq(CBOR_Packet): +class OptThenSeqOf(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] - -= Unframed SEQUENCE with count_from uses the dissected discriminator -from scapy.cbor.cborfields import ( - CBORF_INTEGER, - CBORF_SEQUENCE, - CBORF_SEQUENCE_OF, -) -from scapy.cborpacket import CBOR_Packet - -class CountedUnframed(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( - CBORF_INTEGER("n", 0), - CBORF_SEQUENCE_OF( - "values", [], CBORF_INTEGER, count_from="n" - ), + CBORF_optional(CBORF_UNSIGNED_INTEGER("opt", None)), + CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER), ) -wire = bytes(CountedUnframed(n=2, values=[3, 4])) -assert wire == b"\x02\x03\x04" -pkt = CountedUnframed(wire) -assert pkt.n == 2 -assert pkt.values == [3, 4] +# Terminal SEQUENCE_OF reserves nothing; same-type optional may take the item. +pkt = OptThenSeqOf(b"\x81\x07") +assert pkt.opt == 7 +assert pkt.vals == [] -= Indefinite ARRAY with count_from uses the dissected discriminator -from scapy.cbor.cborfields import ( - CBORF_ARRAY_INDEFINITE, - CBORF_INTEGER, - CBORF_SEQUENCE_OF, -) -from scapy.cborpacket import CBOR_Packet - -class CountedIndef(CBOR_Packet): - CBOR_root = CBORF_ARRAY_INDEFINITE( - CBORF_INTEGER("n", 0), - CBORF_SEQUENCE_OF( - "values", [], CBORF_INTEGER, count_from="n" - ), +class BoolThenSeqOf(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_BOOLEAN("opt", None)), + CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER), ) -wire = bytes(CountedIndef(n=2, values=[3, 4])) -pkt = CountedIndef(wire) -assert pkt.n == 2 -assert pkt.values == [3, 4] +# Type mismatch leaves the item for the trailing SEQUENCE_OF. +pkt = BoolThenSeqOf(b"\x81\x07") +assert pkt.opt is CBOR_ABSENT +assert pkt.vals == [7] = Conditional trailing field follows a false discriminator with optional present from scapy.cbor.cborfields import ( @@ -1208,76 +1152,6 @@ pkt = OptThenReq(b"\x81\x07") assert pkt.opt is CBOR_ABSENT assert pkt.req == 7 -= Count default 5 with wire count 1 decodes on ARRAY SEQUENCE and indefinite -from scapy.cbor.cborfields import ( - CBORF_ARRAY, - CBORF_ARRAY_INDEFINITE, - CBORF_INTEGER, - CBORF_SEQUENCE, - CBORF_SEQUENCE_OF, -) -from scapy.cborpacket import CBOR_Packet - -class CountDef5Array(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER("n", 5), - CBORF_SEQUENCE_OF("values", [], CBORF_INTEGER, count_from="n"), - ) - -class CountDef5Seq(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( - CBORF_INTEGER("n", 5), - CBORF_SEQUENCE_OF("values", [], CBORF_INTEGER, count_from="n"), - ) - -class CountDef5Indef(CBOR_Packet): - CBOR_root = CBORF_ARRAY_INDEFINITE( - CBORF_INTEGER("n", 5), - CBORF_SEQUENCE_OF("values", [], CBORF_INTEGER, count_from="n"), - ) - -pkt = CountDef5Array(b"\x82\x01\x0a") -assert pkt.n == 1 and pkt.values == [10] -pkt = CountDef5Seq(b"\x01\x0a") -assert pkt.n == 1 and pkt.values == [10] -pkt = CountDef5Indef(b"\x9f\x01\x0a\xff") -assert pkt.n == 1 and pkt.values == [10] - -= Count default None does not evaluate int(None) before the discriminator -from scapy.cbor.cborfields import ( - CBORF_ARRAY, - CBORF_ARRAY_INDEFINITE, - CBORF_INTEGER, - CBORF_SEQUENCE, - CBORF_SEQUENCE_OF, -) -from scapy.cborpacket import CBOR_Packet - -class CountDefNoneArray(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER("n", None), - CBORF_SEQUENCE_OF("values", [], CBORF_INTEGER, count_from="n"), - ) - -class CountDefNoneSeq(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( - CBORF_INTEGER("n", None), - CBORF_SEQUENCE_OF("values", [], CBORF_INTEGER, count_from="n"), - ) - -class CountDefNoneIndef(CBOR_Packet): - CBOR_root = CBORF_ARRAY_INDEFINITE( - CBORF_INTEGER("n", None), - CBORF_SEQUENCE_OF("values", [], CBORF_INTEGER, count_from="n"), - ) - -pkt = CountDefNoneArray(b"\x83\x02\x0a\x0b") -assert pkt.n == 2 and pkt.values == [10, 11] -pkt = CountDefNoneSeq(b"\x02\x0a\x0b") -assert pkt.n == 2 and pkt.values == [10, 11] -pkt = CountDefNoneIndef(b"\x9f\x02\x0a\x0b\xff") -assert pkt.n == 2 and pkt.values == [10, 11] - = Conditional disabled by default enables on wire with present value from scapy.cbor.cborfields import ( CBORF_ARRAY, @@ -1313,28 +1187,6 @@ assert pkt.flag == 1 and pkt.last == 8 pkt = CondOffIndef(b"\x9f\x01\x08\xff") assert pkt.flag == 1 and pkt.last == 8 -= Optional before count_from collection reserves the collection items -from scapy.cbor.cborfields import ( - CBORF_ARRAY, - CBORF_INTEGER, - CBORF_SEQUENCE_OF, - CBORF_optional, - CBOR_ABSENT, -) -from scapy.cborpacket import CBOR_Packet - -class OptBeforeCounted(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER("n", 0), - CBORF_optional(CBORF_INTEGER("opt", None)), - CBORF_SEQUENCE_OF("values", [], CBORF_INTEGER, count_from="n"), - ) - -pkt = OptBeforeCounted(b"\x83\x02\x0a\x0b") -assert pkt.n == 2 -assert pkt.opt is CBOR_ABSENT -assert pkt.values == [10, 11] - = Definite ARRAY rejects item count inconsistent with schema from scapy.cbor.cborfields import ( CBORF_ARRAY, @@ -1772,26 +1624,23 @@ class RROptionalThenRequiredPacket(CBOR_Packet): pkt = RROptionalThenRequiredPacket(b"\x81\x07") assert pkt.required_child.value == 7 -= A SEQUENCE_OF before a required conditional field must use count_from -class RRSequenceThenConditionalTail(CBOR_Packet): - CBOR_root = CBORF_ARRAY( += SEQUENCE_OF before a required conditional field is rejected +try: + CBORF_ARRAY( CBORF_UNSIGNED_INTEGER("flag", 0), CBORF_SEQUENCE_OF( "values", [], CBORF_UNSIGNED_INTEGER("item", None), - count_from=lambda pkt: 1, ), CBORF_CONDITIONAL( CBORF_UNSIGNED_INTEGER("tail", None), lambda pkt: pkt.getfieldval("flag") == 1, ), ) - -pkt = RRSequenceThenConditionalTail(b"\x83\x01\x02\x03") -assert pkt.flag == 1 -assert pkt.values == [2] -assert pkt.tail == 3 + assert False, "nonterminal SEQUENCE_OF was accepted" +except ValueError: + pass + Finding 5: recursive CBORF_ANY mutations must invalidate the raw cache @@ -3664,33 +3513,6 @@ assert isinstance(pkt.payload, Raw) or pkt.original.endswith(b"\x03") remain = TwoInts.CBOR_root._dissect_counted(TwoInts(), b"\x01\x02\x03").remaining assert remain == b"\x03" -= Counted unframed SEQUENCE leaves valid invalid and truncated trailing bytes -from scapy.cbor.cborfields import ( - CBORF_INTEGER, - CBORF_SEQUENCE, - CBORF_SEQUENCE_OF, -) -from scapy.cborpacket import CBOR_Packet -from scapy.packet import Raw - -class CountedSequence(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( - CBORF_INTEGER("n", 0), - CBORF_SEQUENCE_OF( - "values", [], CBORF_INTEGER, count_from="n" - ), - ) - -for suffix in (b"\x04", b"\xff", b"\x18"): - wire = b"\x01\x03" + suffix - remain = CountedSequence.CBOR_root._dissect_counted( - CountedSequence(), wire - ).remaining - assert remain == suffix - pkt = CountedSequence(wire) - assert pkt.n == 1 and pkt.values == [3] - assert isinstance(pkt.payload, Raw) and pkt.payload.load == suffix - = CBORF_SEMANTIC_TAG rejects the wrong tag number from scapy.cbor.cborfields import ( CBORF_SEMANTIC_TAG, CBORF_INTEGER, CBOR_Type_Mismatch, From 40294a11774d1aa6bb6911f6b0c38c680c880401 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Thu, 10 Sep 2026 13:23:48 +0200 Subject: [PATCH 58/65] cbor: reserve optionals with live min_items Drop structural_min_items and skipped-optional validation so active conditionals and required tails keep ownership of budgeted items. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborfields.py | 36 ++----------- test/scapy/layers/cbor.uts | 101 ++++++++++++++++++++++++++----------- 2 files changed, 75 insertions(+), 62 deletions(-) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 898e2fb2f94..f2d12d283eb 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -194,11 +194,6 @@ def max_items(self, pkt): # type: (CBOR_Packet) -> int return 1 - def structural_min_items(self, pkt): - # type: (CBOR_Packet) -> int - """Lower bound independent of not-yet-dissected discriminators.""" - return self.min_items(pkt) - def structural_max_items(self, pkt): # type: (CBOR_Packet) -> int """Upper bound independent of not-yet-dissected discriminators.""" @@ -1163,7 +1158,7 @@ def _build_children(self, pkt): def _suffix_reserved(self, pkt, index): # type: (CBOR_Packet, int) -> int return sum( - f.structural_min_items(pkt) for f in self.seq[index + 1:] + f.min_items(pkt) for f in self.seq[index + 1:] ) def _mark_absent(self, pkt, field): @@ -1172,16 +1167,6 @@ def _mark_absent(self, pkt, field): if isinstance(field, CBORF_optional): field._field.mark_absent(pkt) - def _validate_skipped_optional(self, pkt, field, remaining): - # type: (CBOR_Packet, Any, bytes) -> None - """Reject a present-but-skipped optional that is malformed on the wire.""" - if ( - isinstance(field, CBORF_optional) - and remaining - and field._field.matches_next_item(pkt, remaining) - ): - field._field._parse_value(pkt, remaining) - def _dissect_field(self, pkt, field, remaining, max_items=None): # type: (CBOR_Packet, Any, bytes, Optional[int]) -> _CBORParseResult if isinstance(field, CBORF_optional): @@ -1201,7 +1186,7 @@ def _dissect_field(self, pkt, field, remaining, max_items=None): self._mark_absent(pkt, field) return result - def _reject_ambiguous_unbounded_sequences(self): + def _reject_nonterminal_sequence_of(self): # type: () -> None for index, field in enumerate(self.seq): inner = field @@ -1232,7 +1217,6 @@ def _dissect_children_budgeted(self, pkt, s, count): if available < 0 or available < needed: raise CBOR_Decoding_Error("CBOR item count mismatch") if available == 0: - self._validate_skipped_optional(pkt, field, remaining) self._mark_absent(pkt, field) continue result = self._dissect_field( @@ -1272,10 +1256,6 @@ def _dissect_children_streamed(self, pkt, s): except CBOR_Codec_Decoding_Error as e: raise CBOR_Decoding_Error(str(e)) if ahead <= suffix_need: - if ahead == suffix_need: - self._validate_skipped_optional( - pkt, field, remaining - ) self._mark_absent(pkt, field) continue result = self._dissect_field(pkt, field, remaining) @@ -1304,7 +1284,7 @@ class MyCBOR(CBOR_Packet): def __init__(self, *seq, **kwargs): # type: (*Any, **Any) -> None super(CBORF_SEQUENCE, self).__init__(*seq, **kwargs) - self._reject_ambiguous_unbounded_sequences() + self._reject_nonterminal_sequence_of() def _build_counted(self, pkt): # type: (CBOR_Packet) -> _CBORBuildResult @@ -1326,10 +1306,6 @@ def max_items(self, pkt): # type: (CBOR_Packet) -> int return sum(f.max_items(pkt) for f in self.seq) - def structural_min_items(self, pkt): - # type: (CBOR_Packet) -> int - return sum(f.structural_min_items(pkt) for f in self.seq) - def structural_max_items(self, pkt): # type: (CBOR_Packet) -> int return sum(f.structural_max_items(pkt) for f in self.seq) @@ -1360,7 +1336,7 @@ class MyCBOR(CBOR_Packet): def __init__(self, *seq, **kwargs): # type: (*Any, **Any) -> None super(CBORF_ARRAY, self).__init__(*seq, **kwargs) - self._reject_ambiguous_unbounded_sequences() + self._reject_nonterminal_sequence_of() def _build_counted(self, pkt): # type: (CBOR_Packet) -> _CBORBuildResult @@ -2251,10 +2227,6 @@ def max_items(self, pkt): return self.fld.max_items(pkt) return 0 - def structural_min_items(self, pkt): - # type: (CBOR_Packet) -> int - return 0 - def structural_max_items(self, pkt): # type: (CBOR_Packet) -> int return self.fld.structural_max_items(pkt) diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index 61b8d65d170..53b315a5146 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -408,10 +408,9 @@ 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..."). +# When the optional has available==0 because a required trailing field +# reserved the only item, mark the optional absent and let the trailing +# field consume the item (even if outer types match). from scapy.cbor.cborfields import ( CBOR_ABSENT, CBORF_ANY, @@ -978,7 +977,7 @@ assert bytes(pkt) == b"\xa1\x61n\x01" + array item reservation -= Nonterminal unbounded SEQUENCE_OF is rejected at schema construction += Nonterminal SEQUENCE_OF is rejected at schema construction from scapy.cbor.cborfields import ( CBORF_ARRAY, CBORF_SEQUENCE_OF, @@ -990,11 +989,11 @@ try: CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER), CBORF_UNSIGNED_INTEGER("tail", 0), ) - assert False, "nonterminal unbounded SEQUENCE_OF was accepted" + assert False, "nonterminal SEQUENCE_OF was accepted" except ValueError: pass -= Optional wrapper cannot hide a nonterminal unbounded SEQUENCE_OF += Optional wrapper cannot hide a nonterminal SEQUENCE_OF from scapy.cbor.cborfields import ( CBORF_ARRAY, CBORF_SEQUENCE_OF, @@ -1009,11 +1008,11 @@ try: ), CBORF_UNSIGNED_INTEGER("tail", 0), ) - assert False, "optional unbounded SEQUENCE_OF before tail was accepted" + assert False, "optional SEQUENCE_OF before tail was accepted" except ValueError: pass -= Conditional wrapper cannot hide a nonterminal unbounded SEQUENCE_OF += Conditional wrapper cannot hide a nonterminal SEQUENCE_OF from scapy.cbor.cborfields import ( CBORF_ARRAY, CBORF_CONDITIONAL, @@ -1029,11 +1028,11 @@ try: ), CBORF_UNSIGNED_INTEGER("tail", 0), ) - assert False, "conditional unbounded SEQUENCE_OF before tail was accepted" + assert False, "conditional SEQUENCE_OF before tail was accepted" except ValueError: pass -= Nested transparent wrappers cannot hide a nonterminal unbounded SEQUENCE_OF += Nested transparent wrappers cannot hide a nonterminal SEQUENCE_OF from scapy.cbor.cborfields import ( CBORF_ARRAY, CBORF_CONDITIONAL, @@ -1052,11 +1051,11 @@ try: ), CBORF_UNSIGNED_INTEGER("tail", 0), ) - assert False, "nested wrapped unbounded SEQUENCE_OF before tail was accepted" + assert False, "nested wrapped SEQUENCE_OF before tail was accepted" except ValueError: pass -= Terminal optional unbounded SEQUENCE_OF remains a valid schema += Terminal optional SEQUENCE_OF remains a valid schema from scapy.cbor.cborfields import ( CBORF_ARRAY, CBORF_SEQUENCE_OF, @@ -1152,6 +1151,52 @@ pkt = OptThenReq(b"\x81\x07") assert pkt.opt is CBOR_ABSENT assert pkt.req == 7 += Active conditional reserves against a same-type optional +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_CONDITIONAL, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, + CBOR_ABSENT, +) +from scapy.cborpacket import CBOR_Packet + +class ConditionalTail(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("flag", 0), + CBORF_optional(CBORF_UNSIGNED_INTEGER("opt", None)), + CBORF_CONDITIONAL( + CBORF_UNSIGNED_INTEGER("req", 0), + lambda pkt: pkt.flag == 1, + ), + ) + +pkt = ConditionalTail(b"\x82\x01\x07") +assert pkt.flag == 1 +assert pkt.opt is CBOR_ABSENT +assert pkt.req == 7 + += Skipped definite-only optional does not poison a required bytestring +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_BYTE_STRING, + CBORF_optional, + CBOR_ABSENT, +) +from scapy.cborpacket import CBOR_Packet + +class OptionalDefinite(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional( + CBORF_BYTE_STRING("opt", None, definite_only=True) + ), + CBORF_BYTE_STRING("value", b""), + ) + +pkt = OptionalDefinite(b"\x81\x5f\x41a\xff") +assert pkt.opt is CBOR_ABSENT +assert pkt.value == b"a" + = Conditional disabled by default enables on wire with present value from scapy.cbor.cborfields import ( CBORF_ARRAY, @@ -1207,7 +1252,7 @@ try: except CBOR_Decoding_Error: pass -= Indefinite array rejects nonterminal unbounded SEQUENCE_OF += Indefinite array rejects nonterminal SEQUENCE_OF from scapy.cbor.cborfields import ( CBORF_ARRAY_INDEFINITE, CBORF_SEQUENCE_OF, @@ -1219,7 +1264,7 @@ try: CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER), CBORF_UNSIGNED_INTEGER("tail", 0), ) - assert False, "nonterminal unbounded SEQUENCE_OF was accepted" + assert False, "nonterminal SEQUENCE_OF was accepted" except ValueError: pass @@ -1571,7 +1616,7 @@ assert bytes(pkt) == b"\x82\xf6\x01" + Finding 4: positional arrays must reserve items for later required fields -= Definite arrays reject nonterminal unbounded SEQUENCE_OF += Definite arrays reject nonterminal SEQUENCE_OF try: CBORF_ARRAY( CBORF_SEQUENCE_OF( @@ -1581,11 +1626,11 @@ try: ), CBORF_UNSIGNED_INTEGER("tail", None), ) - assert False, "nonterminal unbounded SEQUENCE_OF accepted" + assert False, "nonterminal SEQUENCE_OF accepted" except ValueError: pass -= Indefinite arrays reject nonterminal unbounded SEQUENCE_OF += Indefinite arrays reject nonterminal SEQUENCE_OF try: CBORF_ARRAY_INDEFINITE( CBORF_SEQUENCE_OF( @@ -1595,7 +1640,7 @@ try: ), CBORF_UNSIGNED_INTEGER("tail", None), ) - assert False, "nonterminal unbounded SEQUENCE_OF accepted" + assert False, "nonterminal SEQUENCE_OF accepted" except ValueError: pass @@ -2116,7 +2161,7 @@ pkt = OptionalPacketThenAny(b"\x81\xf5") assert pkt.getfieldval("child") is CBOR_ABSENT assert pkt.required.val is True -= Nonterminal unbounded SEQUENCE_OF is rejected in unframed sequences += Nonterminal SEQUENCE_OF is rejected in unframed sequences from scapy.cbor.cborfields import ( CBORF_SEQUENCE, CBORF_SEQUENCE_OF, @@ -2129,7 +2174,7 @@ try: CBORF_SEQUENCE_OF("items", [], CBORF_UNSIGNED_INTEGER), CBORF_TEXT_STRING("tail", ""), ) - assert False, "nonterminal unbounded SEQUENCE_OF accepted" + assert False, "nonterminal SEQUENCE_OF accepted" except ValueError: pass @@ -3561,8 +3606,7 @@ try: 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 += Budget-skipped optional yields a matching item to trailing CBORF_ANY from scapy.cbor.cborfields import ( CBORF_ANY, CBORF_ARRAY, @@ -3581,14 +3625,11 @@ class OptionalTaggedBeforeAny(CBOR_Packet): CBORF_ANY("fallback", None), ) -try: - OptionalTaggedBeforeAny(b"\x81\xc1\x61x") -except CBOR_Decoding_Error: - pass -else: - raise AssertionError("Malformed tagged data migrated into the fallback field") +# Outer type matches the optional, but the sole item is reserved for ANY. +pkt = OptionalTaggedBeforeAny(b"\x81\xc1\x61x") +assert pkt.getfieldval("tagged_value") is CBOR_ABSENT +assert pkt.fallback is not None -# Matching well-formed optional still yields to reserved trailing ANY. ok = OptionalTaggedBeforeAny(b"\x81\xc1\x01") assert ok.getfieldval("tagged_value") is CBOR_ABSENT assert ok.fallback is not None From bf2e41d61df0dced5144780518b123492031b4e6 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Thu, 10 Sep 2026 21:25:47 +0200 Subject: [PATCH 59/65] cbor: fix absent cache and greedy unframed sequences Skip top-level CBOR_ABSENT fingerprints, require direct-terminal SEQUENCE_OF, and drop SEQUENCE suffix lookahead. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborfields.py | 83 ++++------- scapy/cborpacket.py | 3 +- test/scapy/layers/cbor.uts | 291 ++++++++++++++++++++++++++++--------- 3 files changed, 256 insertions(+), 121 deletions(-) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index f2d12d283eb..2b8715d2189 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -1085,20 +1085,14 @@ def m2i(self, pkt, s): def _encode_leaf(self, x): # type: (Any) -> bytes - if isinstance(x, CBOR_FLOAT): - return CBORcodec_SIMPLE_AND_FLOAT.enc(float(x.val)) 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): @@ -1155,12 +1149,6 @@ def _build_children(self, pkt): total_items += result.items return b"".join(parts), total_items - def _suffix_reserved(self, pkt, index): - # type: (CBOR_Packet, int) -> int - return sum( - f.min_items(pkt) for f in self.seq[index + 1:] - ) - def _mark_absent(self, pkt, field): # type: (CBOR_Packet, Any) -> None """Record that an optional field was not present on the wire.""" @@ -1188,19 +1176,8 @@ def _dissect_field(self, pkt, field, remaining, max_items=None): def _reject_nonterminal_sequence_of(self): # type: () -> None - for index, field in enumerate(self.seq): - 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): - continue - # SEQUENCE_OF is always terminal (consumes the remainder). - if index != len(self.seq) - 1: + for field in self.seq[:-1]: + if isinstance(field, CBORF_SEQUENCE_OF): raise ValueError( "CBORF_SEQUENCE_OF must be the last field " "in the sequence" @@ -1211,7 +1188,9 @@ def _dissect_children_budgeted(self, pkt, s, count): remaining = s items_left = count for index, field in enumerate(self.seq): - reserved = self._suffix_reserved(pkt, index) + reserved = sum( + f.min_items(pkt) for f in self.seq[index + 1:] + ) available = items_left - reserved needed = field.min_items(pkt) if available < 0 or available < needed: @@ -1234,30 +1213,10 @@ def _dissect_children_budgeted(self, pkt, s, count): def _dissect_children_streamed(self, pkt, s): # type: (CBOR_Packet, bytes) -> Tuple[bytes, int] - """Consume schema fields from *s* without pre-counting trailing bytes.""" + """Consume schema fields greedily left-to-right; leave trailing bytes.""" remaining = s total_items = 0 - for index, field in enumerate(self.seq): - if isinstance(field, CBORF_optional): - suffix_need = self._suffix_reserved(pkt, index) - if ( - not remaining - or not field._field.matches_next_item(pkt, remaining) - ): - self._mark_absent(pkt, field) - continue - if suffix_need > 0: - try: - ahead = cbor_count_items( - remaining, - max_count=suffix_need + 1, - until_break=False, - ) - except CBOR_Codec_Decoding_Error as e: - raise CBOR_Decoding_Error(str(e)) - if ahead <= suffix_need: - self._mark_absent(pkt, field) - continue + for field in self.seq: result = self._dissect_field(pkt, field, remaining) remaining = result.remaining total_items += result.items @@ -1269,8 +1228,13 @@ 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`). + CBOR items with greedy left-to-right parsing and no suffix lookahead. + Use it when a schema is a field list without a major-type-4 envelope + (ASN.1 SEQUENCE analogy belongs on :class:`CBORF_ARRAY`). + + Same-type optional-then-required schemas are ambiguous on the wire; + prefer :class:`CBORF_ARRAY` (item budget) or :class:`CBORF_CONDITIONAL` + with a previously decoded discriminator. Example:: @@ -1587,10 +1551,11 @@ class CBORF_SEQUENCE_OF(_CBORF_HOMOGENEOUS): :class:`~typing.Generic` reserves that name on Python 3.7. Pass only one of ``pkt_cls`` / ``next_cls_cb``. - ``SEQUENCE_OF`` must be the last field in its enclosing positional - schema (``CBORF_SEQUENCE`` / ``CBORF_ARRAY``). It consumes the - remainder until break or exhaustion. Use ``max_count`` to cap - decoding (defaults to ``conf.max_list_count``). + ``SEQUENCE_OF`` is an unframed remainder consumer: place it directly + as the final child of ``CBORF_SEQUENCE`` / ``CBORF_ARRAY`` (not inside + ``CBORF_optional`` or ``CBORF_CONDITIONAL``). It consumes until break + or exhaustion. Use ``max_count`` to cap decoding (defaults to + ``conf.max_list_count``). """ CBOR_tag = None _empty_repr = "()" @@ -2150,6 +2115,11 @@ def __init__(self, field): "CBORF_optional requires CBORF_field or CBORF_SEMANTIC_TAG; " "got %r" % (type(field).__name__,) ) + if isinstance(field, CBORF_SEQUENCE_OF): + raise ValueError( + "CBORF_SEQUENCE_OF cannot be wrapped; " + "place it directly as the final positional field" + ) self._field = field def __getattr__(self, attr): @@ -2193,6 +2163,11 @@ def __init__(self, cond, # type: Callable[[Packet], bool] ): # type: (...) -> None + if isinstance(fld, CBORF_SEQUENCE_OF): + raise ValueError( + "CBORF_SEQUENCE_OF cannot be wrapped; " + "place it directly as the final positional field" + ) fields.ConditionalField.__init__(self, fld, cond) def __repr__(self): diff --git a/scapy/cborpacket.py b/scapy/cborpacket.py index a5e3305defa..52ed1f514e0 100644 --- a/scapy/cborpacket.py +++ b/scapy/cborpacket.py @@ -143,8 +143,9 @@ def do_dissect(self, s): if f.name not in self.fields: continue fval = self.fields[f.name] + # Absent scalars are not mutable fingerprints; storing CBOR_ABSENT + # here would fail validation against the generic None fingerprint. if fval is CBOR_ABSENT: - self.raw_packet_cache_fields[f.name] = CBOR_ABSENT continue if getattr(f, "isconditional", False) and fval is None: continue diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index 53b315a5146..a0d32de485d 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -993,89 +993,101 @@ try: except ValueError: pass -= Optional wrapper cannot hide a nonterminal SEQUENCE_OF += Optional cannot wrap 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), + CBORF_optional( + CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER) ) - assert False, "optional SEQUENCE_OF before tail was accepted" + assert False, "optional SEQUENCE_OF was accepted" except ValueError: pass -= Conditional wrapper cannot hide a nonterminal SEQUENCE_OF += Conditional cannot wrap 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), + CBORF_CONDITIONAL( + CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER), + lambda pkt: True, ) - assert False, "conditional SEQUENCE_OF before tail was accepted" + assert False, "conditional SEQUENCE_OF was accepted" except ValueError: pass -= Nested transparent wrappers cannot hide a nonterminal SEQUENCE_OF += Direct terminal SEQUENCE_OF respects definite array item budget from scapy.cbor.cborfields import ( CBORF_ARRAY, - CBORF_CONDITIONAL, CBORF_SEQUENCE_OF, CBORF_UNSIGNED_INTEGER, - CBORF_optional, ) +from scapy.cborpacket import CBOR_Packet +from scapy.packet import Raw -try: - CBORF_ARRAY( - CBORF_CONDITIONAL( - CBORF_optional( - CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER) - ), - lambda pkt: True, - ), - CBORF_UNSIGNED_INTEGER("tail", 0), +class DirectTerminal(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("n", 0), + CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER), ) - assert False, "nested wrapped SEQUENCE_OF before tail was accepted" -except ValueError: - pass -= Terminal optional SEQUENCE_OF remains a valid schema +pkt = DirectTerminal() +result = DirectTerminal.CBOR_root._dissect_counted( + pkt, b"\x82\x01\x0a\x0b" +) +assert pkt.n == 1 +assert pkt.vals == [10] +assert result.remaining == b"\x0b" + +pkt = DirectTerminal(b"\x82\x01\x0a\x0b") +assert pkt.n == 1 +assert pkt.vals == [10] +assert isinstance(pkt.payload, Raw) and pkt.payload.load == b"\x0b" + += Direct terminal SEQUENCE_OF in indefinite ARRAY stops at break from scapy.cbor.cborfields import ( - CBORF_ARRAY, + CBORF_ARRAY_INDEFINITE, CBORF_SEQUENCE_OF, CBORF_UNSIGNED_INTEGER, - CBORF_optional, ) from scapy.cborpacket import CBOR_Packet -class TerminalOptSeq(CBOR_Packet): - CBOR_root = CBORF_ARRAY( +class DirectTerminalIndef(CBOR_Packet): + CBOR_root = CBORF_ARRAY_INDEFINITE( CBORF_UNSIGNED_INTEGER("n", 0), - CBORF_optional( - CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER) - ), + CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER), ) -pkt = TerminalOptSeq(b"\x83\x01\x0a\x0b") +pkt = DirectTerminalIndef(b"\x9f\x01\x0a\x0b\xff") assert pkt.n == 1 assert pkt.vals == [10, 11] += Empty terminal SEQUENCE_OF leaves an empty list +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_SEQUENCE_OF, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cborpacket import CBOR_Packet + +class EmptyTerminal(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("n", 0), + CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER), + ) + +pkt = EmptyTerminal(b"\x81\x05") +assert pkt.n == 5 +assert pkt.vals == [] + = Optional before trailing SEQUENCE_OF does not invent a reserved count from scapy.cbor.cborfields import ( CBORF_ARRAY, @@ -2055,7 +2067,6 @@ from scapy.cbor.cborfields import ( CBORF_ANY, CBORF_ARRAY, CBORF_BOOLEAN, - CBORF_SEQUENCE, CBORF_optional, CBOR_ABSENT, ) @@ -2067,24 +2078,13 @@ class OptionalBoolThenAnyArray(CBOR_Packet): CBORF_ANY("required", CBOR_ABSENT), ) -class OptionalBoolThenAnySequence(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( - CBORF_optional(CBORF_BOOLEAN("maybe", None)), - CBORF_ANY("required", CBOR_ABSENT), - ) - -# 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. +# One array item is reserved for the required trailing field, so the +# optional Boolean stays absent even though the item is Boolean. arr = OptionalBoolThenAnyArray(b"\x81\xf5") assert arr.getfieldval("maybe") is CBOR_ABSENT assert arr.required.val is True assert bytes(arr) == b"\x81\xf5" -seq = OptionalBoolThenAnySequence(b"\xf5") -assert seq.getfieldval("maybe") is CBOR_ABSENT -assert seq.required.val is True -assert bytes(seq) == b"\xf5" - = Indefinite arrays reserve the final item for a required ANY field from scapy.cbor.cborfields import ( CBORF_ANY, @@ -2111,7 +2111,6 @@ from scapy.cbor.cborfields import ( CBORF_ANY, CBORF_ARRAY, CBORF_BOOLEAN, - CBORF_SEQUENCE, CBORF_optional, CBOR_ABSENT, ) @@ -2123,20 +2122,10 @@ class OptionalAnyThenBoolArray(CBOR_Packet): CBORF_BOOLEAN("required", None), ) -class OptionalAnyThenBoolSequence(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( - CBORF_optional(CBORF_ANY("maybe", CBOR_ABSENT)), - CBORF_BOOLEAN("required", None), - ) - arr = OptionalAnyThenBoolArray(b"\x81\xf5") assert arr.getfieldval("maybe") is CBOR_ABSENT assert arr.required is True -seq = OptionalAnyThenBoolSequence(b"\xf5") -assert seq.getfieldval("maybe") is CBOR_ABSENT -assert seq.required is True - = Optional packet does not consume an item reserved for a trailing required ANY from scapy.cbor.cborfields import ( CBORF_ANY, @@ -2682,6 +2671,176 @@ child = SeqChild(overlong) assert child.raw_packet_cache == overlong assert bytes(child) == overlong += Absent optional scalar preserves non-preferred following encoding +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_BOOLEAN, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, + CBOR_ABSENT, +) +from scapy.cborpacket import CBOR_Packet + +class OptionalThenInt(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_BOOLEAN("maybe", None)), + CBORF_UNSIGNED_INTEGER("value", 0), + ) + +raw = b"\x81\x18\x00" +pkt = OptionalThenInt(raw) +assert pkt.maybe is CBOR_ABSENT +assert pkt.value == 0 +assert bytes(pkt) == raw +assert pkt.raw_packet_cache == raw + +pkt = OptionalThenInt(raw) +pkt.value = 1 +assert bytes(pkt) == b"\x81\x01" + +pkt = OptionalThenInt(raw) +pkt.maybe = True +rebuilt = bytes(pkt) +assert rebuilt != raw +assert OptionalThenInt(rebuilt).maybe is True + += Absent optional integer preserves overlong follower +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, + CBOR_ABSENT, +) +from scapy.cborpacket import CBOR_Packet + +class OptIntThenInt(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_UNSIGNED_INTEGER("opt", None)), + CBORF_UNSIGNED_INTEGER("value", 0), + ) + +raw = b"\x81\x18\x00" +pkt = OptIntThenInt(raw) +assert pkt.opt is CBOR_ABSENT +assert pkt.value == 0 +assert bytes(pkt) == raw + += Multiple absent optionals preserve untouched raw cache +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_BOOLEAN, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, + CBOR_ABSENT, +) +from scapy.cborpacket import CBOR_Packet + +class MultiAbsent(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_BOOLEAN("a", None)), + CBORF_optional(CBORF_BOOLEAN("b", None)), + CBORF_UNSIGNED_INTEGER("value", 0), + ) + +raw = b"\x81\x18\x00" +pkt = MultiAbsent(raw) +assert pkt.a is CBOR_ABSENT and pkt.b is CBOR_ABSENT +assert pkt.value == 0 +assert bytes(pkt) == raw + += Nested packet with absent optional preserves exact child bytes +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_BOOLEAN, + CBORF_PACKET, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, + CBOR_ABSENT, +) +from scapy.cborpacket import CBOR_Packet + +class NestedOptChild(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_BOOLEAN("maybe", None)), + CBORF_UNSIGNED_INTEGER("value", 0), + ) + +class NestedOptParent(CBOR_Packet): + CBOR_root = CBORF_PACKET("child", None, NestedOptChild) + +raw = b"\x81\x18\x00" +pkt = NestedOptParent(raw) +assert pkt.child.maybe is CBOR_ABSENT +assert pkt.child.value == 0 +assert bytes(pkt) == raw +assert pkt.child.raw_packet_cache == raw + += Absent optional map member preserves non-preferred known value +from scapy.cbor.cborfields import ( + CBORF_BOOLEAN, + CBORF_MAP, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, + CBOR_ABSENT, +) +from scapy.cborpacket import CBOR_Packet + +class OptMap(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_optional(CBORF_BOOLEAN("maybe", None)), + CBORF_UNSIGNED_INTEGER("value", 0), + ) + +# map with only "value": overlong 0 +raw = b"\xa1\x65value\x18\x00" +pkt = OptMap(raw) +assert pkt.maybe is CBOR_ABSENT +assert pkt.value == 0 +assert bytes(pkt) == raw + += Unframed SEQUENCE optional parsing is greedy left-to-right +from scapy.cbor.cbor import CBOR_Decoding_Error +from scapy.cbor.cborfields import ( + CBORF_BOOLEAN, + CBORF_SEQUENCE, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, + CBOR_ABSENT, +) +from scapy.cborpacket import CBOR_Packet + +class GreedySequence(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_optional(CBORF_UNSIGNED_INTEGER("optional", None)), + CBORF_UNSIGNED_INTEGER("required", 0), + ) + +pkt = GreedySequence(b"\x01\x02") +assert pkt.optional == 1 +assert pkt.required == 2 + +try: + GreedySequence(b"\x01") + assert False, "ambiguous single unsigned item must fail under greedy SEQUENCE" +except CBOR_Decoding_Error: + pass + +class TypedSequence(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_optional(CBORF_BOOLEAN("optional", None)), + CBORF_UNSIGNED_INTEGER("required", 0), + ) + +pkt = TypedSequence(b"\x07") +assert pkt.optional is CBOR_ABSENT +assert pkt.required == 7 + +pkt = TypedSequence() +result = TypedSequence.CBOR_root._dissect_counted(pkt, b"\x07\x18") +assert pkt.optional is CBOR_ABSENT +assert pkt.required == 7 +assert result.remaining == b"\x18" + # 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). From 6151e14bca9f100ff596fcc930566fc725f8e7d0 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Thu, 10 Sep 2026 21:44:35 +0200 Subject: [PATCH 60/65] cbor: reject tagged SEQUENCE_OF; slim optional dissect Fail SEMANTIC_TAG(SEQUENCE_OF) at construction, let optional own presence matching, and drop the single-use _mark_absent helper. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborfields.py | 42 +++++++--------- test/scapy/layers/cbor.uts | 100 +++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 25 deletions(-) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 2b8715d2189..a82cd8f4a60 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -1149,30 +1149,13 @@ def _build_children(self, pkt): 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 field was not present on the wire.""" - if isinstance(field, CBORF_optional): - field._field.mark_absent(pkt) - def _dissect_field(self, pkt, field, remaining, max_items=None): # type: (CBOR_Packet, Any, bytes, Optional[int]) -> _CBORParseResult - if isinstance(field, CBORF_optional): - if not remaining or not field._field.matches_next_item( - pkt, remaining - ): - self._mark_absent(pkt, field) - return _CBORParseResult(remaining=remaining, items=0) - result = field._dissect_counted(pkt, remaining) - elif isinstance(field, CBORF_SEQUENCE_OF): - result = field._dissect_counted( + if isinstance(field, CBORF_SEQUENCE_OF): + return field._dissect_counted( pkt, remaining, max_items=max_items ) - else: - result = field._dissect_counted(pkt, remaining) - if result.items == 0: - self._mark_absent(pkt, field) - return result + return field._dissect_counted(pkt, remaining) def _reject_nonterminal_sequence_of(self): # type: () -> None @@ -1196,7 +1179,8 @@ def _dissect_children_budgeted(self, pkt, s, count): if available < 0 or available < needed: raise CBOR_Decoding_Error("CBOR item count mismatch") if available == 0: - self._mark_absent(pkt, field) + if isinstance(field, CBORF_optional): + field._field.mark_absent(pkt) continue result = self._dissect_field( pkt, field, remaining, max_items=available @@ -1551,10 +1535,13 @@ class CBORF_SEQUENCE_OF(_CBORF_HOMOGENEOUS): :class:`~typing.Generic` reserves that name on Python 3.7. Pass only one of ``pkt_cls`` / ``next_cls_cb``. - ``SEQUENCE_OF`` is an unframed remainder consumer: place it directly - as the final child of ``CBORF_SEQUENCE`` / ``CBORF_ARRAY`` (not inside - ``CBORF_optional`` or ``CBORF_CONDITIONAL``). It consumes until break - or exhaustion. Use ``max_count`` to cap decoding (defaults to + ``SEQUENCE_OF`` represents an unframed sequence of zero or more CBOR + items. Because it consumes the remaining item budget/input, it must be + a direct final child of a positional ``CBORF_SEQUENCE`` or + ``CBORF_ARRAY``. + + It must not be wrapped in ``CBORF_optional``, ``CBORF_CONDITIONAL``, or + ``CBORF_SEMANTIC_TAG``. Use ``max_count`` to cap decoding (defaults to ``conf.max_list_count``). """ CBOR_tag = None @@ -2004,6 +1991,11 @@ def __init__(self, if tag_num < 0 or tag_num > CBOR_UINT64_MAX: raise CBOR_Encoding_Error( "Semantic tag number out of uint64 range") + if isinstance(inner_field, CBORF_SEQUENCE_OF): + raise ValueError( + "CBORF_SEQUENCE_OF cannot be wrapped; " + "place it directly as the final positional field" + ) self.tag_num = tag_num self.inner_field = inner_field diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index a0d32de485d..2ffa3a42827 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -1024,6 +1024,82 @@ try: except ValueError: pass += CBORF_SEMANTIC_TAG cannot wrap CBORF_SEQUENCE_OF +from scapy.cbor.cborfields import ( + CBORF_SEMANTIC_TAG, + CBORF_SEQUENCE_OF, + CBORF_UNSIGNED_INTEGER, +) + +try: + CBORF_SEMANTIC_TAG( + 1, + CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER), + ) + assert False, "semantic-tagged SEQUENCE_OF should be rejected" +except ValueError: + pass + += Optional semantic tag cannot hide CBORF_SEQUENCE_OF +from scapy.cbor.cborfields import ( + CBORF_SEMANTIC_TAG, + CBORF_SEQUENCE_OF, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, +) + +try: + CBORF_optional( + CBORF_SEMANTIC_TAG( + 1, + CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER), + ) + ) + assert False, "wrapped semantic-tagged SEQUENCE_OF should be rejected" +except ValueError: + pass + += Tagged scalar and ARRAY_OF remain valid semantic-tag content +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_ARRAY_OF, + CBORF_SEMANTIC_TAG, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cborpacket import CBOR_Packet + +class TaggedInt(CBOR_Packet): + CBOR_root = CBORF_SEMANTIC_TAG( + 1, CBORF_UNSIGNED_INTEGER("value", 0) + ) + +pkt = TaggedInt(b"\xc1\x0a") +assert pkt.value == 10 +assert bytes(pkt) == b"\xc1\x0a" + +class TaggedArrayOf(CBOR_Packet): + CBOR_root = CBORF_SEMANTIC_TAG( + 1, + CBORF_ARRAY_OF("vals", [], CBORF_UNSIGNED_INTEGER), + ) + +pkt = TaggedArrayOf(b"\xc1\x82\x01\x02") +assert pkt.vals == [1, 2] +assert bytes(pkt) == b"\xc1\x82\x01\x02" + +class TaggedStruct(CBOR_Packet): + CBOR_root = CBORF_SEMANTIC_TAG( + 1, + CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("a", 0), + CBORF_UNSIGNED_INTEGER("b", 0), + ), + ) + +pkt = TaggedStruct(b"\xc1\x82\x01\x02") +assert pkt.a == 1 and pkt.b == 2 +assert bytes(pkt) == b"\xc1\x82\x01\x02" + = Direct terminal SEQUENCE_OF respects definite array item budget from scapy.cbor.cborfields import ( CBORF_ARRAY, @@ -2835,12 +2911,36 @@ pkt = TypedSequence(b"\x07") assert pkt.optional is CBOR_ABSENT assert pkt.required == 7 +pkt = TypedSequence(b"\xf5\x07") +assert pkt.optional is True +assert pkt.required == 7 + pkt = TypedSequence() result = TypedSequence.CBOR_root._dissect_counted(pkt, b"\x07\x18") assert pkt.optional is CBOR_ABSENT assert pkt.required == 7 assert result.remaining == b"\x18" += Optional-only SEQUENCE marks absent on empty input +from scapy.cbor.cborfields import ( + CBORF_BOOLEAN, + CBORF_SEQUENCE, + CBORF_optional, + CBOR_ABSENT, +) +from scapy.cborpacket import CBOR_Packet + +class OptionalOnly(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_optional(CBORF_BOOLEAN("maybe", None)), + ) + +# Packet(b"") skips dissect (empty bytes are falsy); drive the schema directly. +pkt = OptionalOnly() +result = OptionalOnly.CBOR_root._dissect_counted(pkt, b"") +assert pkt.maybe is CBOR_ABSENT +assert result.remaining == b"" + # CBORF_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). From 2366a19ba18764070d27902e8266a0ca673f61c6 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Fri, 11 Sep 2026 08:45:05 +0200 Subject: [PATCH 61/65] cbor: typed key/fingerprint kinds and enum-safe compares Use CBOR_KeyKind norms and CBOR_FingerprintKind cache tags, drop redundant int() enum casts, half floats via struct >e, and make EnumElement deepcopy a singleton. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cbor.py | 161 ++++++++++++------ scapy/cbor/cborcodec.py | 238 +++++++++++++-------------- scapy/cbor/cborfields.py | 73 ++++---- scapy/utils.py | 8 + test/scapy/layers/cbor_test_utils.py | 62 +++---- 5 files changed, 300 insertions(+), 242 deletions(-) diff --git a/scapy/cbor/cbor.py b/scapy/cbor/cbor.py index 07196d50461..05054356bbd 100644 --- a/scapy/cbor/cbor.py +++ b/scapy/cbor/cbor.py @@ -14,6 +14,7 @@ from typing import ( Any, Dict, + FrozenSet, Generic, List, Optional, @@ -319,6 +320,70 @@ class CBOR_FloatAI(metaclass=Enum_metaclass): DOUBLE = 27 # IEEE binary64 +class CBOR_KeyKind(metaclass=Enum_metaclass): + """Discriminator for hashable RFC 8949 map-key norms. + + Norms are tuples ``(kind, ...)`` (not dicts) so they remain hashable for + ``frozenset`` map-key identity. ``kind`` is always a ``CBOR_KeyKind``. + """ + name = "CBOR_KEY_KIND" + BOOL = 1 + NULL = 2 + UNDEF = 3 + INT = 4 + BSTR = 5 + TSTR = 6 + ARRAY = 7 + MAP = 8 + TAG = 9 + SIMPLE = 10 + OBJ = 11 + OTHER = 12 + NAN = 13 + FINITE = 14 + + +class CBOR_FingerprintKind(CBOR_KeyKind): + """Discriminator for ``CBORF_ANY`` raw-cache fingerprints. + + Extends :class:`CBOR_KeyKind` with rebuild-sensitive tags that map-key + norms do not need (sentinels, signed zero/inf, ``_encoded`` floats, + Python containers). + """ + name = "CBOR_FINGERPRINT_KIND" + SENTINEL = 15 + FLOAT = 16 + INF = 17 + ZERO = 18 + MAPDATA = 19 + LIST = 20 + DICT = 21 + PY = 22 + + +# Recursive hashable RFC 8949 map-key norm. +# First element is a ``CBOR_KeyKind`` ``EnumElement`` (typed ``Any`` because +# ``Enum_metaclass`` members are ``int`` in the class body). +_CBORKeyNorm = Union[ + Tuple[Any, None], + Tuple[Any, bool], + Tuple[Any, int], + Tuple[Any, bytes], + Tuple[Any, str], + Tuple[Any, float], + Tuple[Any, int, int], + Tuple[Any, Tuple["_CBORKeyNorm", ...]], + Tuple[Any, FrozenSet[Tuple["_CBORKeyNorm", "_CBORKeyNorm"]]], + Tuple[Any, int, "_CBORKeyNorm"], + Tuple[Any, str, "_CBORKeyNorm"], + Tuple[Any, str, str], +] + +# Recursive rebuild-relevant fingerprint for ``CBORF_ANY`` raw-cache state. +# Kind discriminator is ``CBOR_FingerprintKind`` (same EnumElement typing note). +_CBORFingerprint = Tuple[Any, ...] + + CBOR_UINT64_MAX = (1 << 64) - 1 @@ -475,7 +540,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 = set() # type: Set[Any] + used_norms = set() # type: Set[_CBORKeyNorm] for key, value in self._pairs: norm = _cbor_key_norm(key) if norm in used_norms: @@ -766,15 +831,15 @@ def _cbor_float_wire_parts(encoded): if not wire: raise ValueError("empty CBOR float encoding") ai = wire[0] & 0x1f - if ai == int(CBOR_FloatAI.HALF): + if ai == CBOR_FloatAI.HALF: if len(wire) < 3: raise ValueError("truncated half float") return ai, struct.unpack(">H", wire[1:3])[0] - if ai == int(CBOR_FloatAI.SINGLE): + if ai == CBOR_FloatAI.SINGLE: if len(wire) < 5: raise ValueError("truncated single float") return ai, struct.unpack(">I", wire[1:5])[0] - if ai == int(CBOR_FloatAI.DOUBLE): + if ai == CBOR_FloatAI.DOUBLE: if len(wire) < 9: raise ValueError("truncated double float") return ai, struct.unpack(">Q", wire[1:9])[0] @@ -782,7 +847,7 @@ def _cbor_float_wire_parts(encoded): def _cbor_float_key_identity(value, encoded=None): - # type: (float, Optional[bytes]) -> Tuple[Any, ...] + # type: (float, Optional[bytes]) -> _CBORKeyNorm """Return RFC 8949 floating-point map-key identity for *value*. Finite ``+0.0`` / ``-0.0`` collapse. NaNs compare by sign and @@ -791,27 +856,15 @@ def _cbor_float_key_identity(value, encoded=None): and sign survive Python's NaN canonicalization. """ if encoded is not None: - from scapy.cbor.cborcodec import _cbor_float_from_bits + from scapy.cbor.cborcodec import ( + _cbor_float_from_bits, + _cbor_nan_components, + ) ai, bits = _cbor_float_wire_parts(encoded) - if ai == int(CBOR_FloatAI.HALF): - sign = (bits >> 15) & 0x1 - exponent = (bits >> 10) & 0x1f - fraction = bits & 0x3ff - if exponent == 31 and fraction: - return ("nan", sign, fraction << 42) - elif ai == int(CBOR_FloatAI.SINGLE): - sign = (bits >> 31) & 0x1 - exponent = (bits >> 23) & 0xff - fraction = bits & 0x7fffff - if exponent == 0xff and fraction: - return ("nan", sign, fraction << 29) - else: - # DOUBLE - sign = (bits >> 63) & 0x1 - exponent = (bits >> 52) & 0x7ff - fraction = bits & ((1 << 52) - 1) - if exponent == 0x7ff and fraction: - return ("nan", sign, fraction) + comps = _cbor_nan_components(ai, bits) + if comps is not None: + sign, significand52 = comps + return (CBOR_KeyKind.NAN, sign, significand52) return _cbor_float_key_identity(_cbor_float_from_bits(ai, bits)) fval = float(value) @@ -819,14 +872,14 @@ def _cbor_float_key_identity(value, encoded=None): bits = struct.unpack(">Q", struct.pack(">d", fval))[0] sign = (bits >> 63) & 0x1 significand = bits & ((1 << 52) - 1) - return ("nan", sign, significand) + return (CBOR_KeyKind.NAN, sign, significand) if fval == 0.0: - return ("finite", 0.0) - return ("finite", fval) + return (CBOR_KeyKind.FINITE, 0.0) + return (CBOR_KeyKind.FINITE, fval) def _cbor_key_norm(value): - # type: (Any) -> Any + # type: (Any) -> _CBORKeyNorm """Return a hashable RFC 8949 map-key equivalence form for *value*. Integers and floats remain distinct groups. Floating ``+0.0`` and @@ -834,29 +887,35 @@ def _cbor_key_norm(value): significand match across widths. Arrays compare order-sensitively; maps compare as unordered pairs of norms. Semantic tags require the same tag number and an equivalent tagged value. + + Each norm is a tuple starting with :class:`CBOR_KeyKind` (not a dict), + so norms stay hashable for map ``frozenset`` identity. """ if isinstance(value, CBOR_Object): if isinstance(value, (CBOR_TRUE, CBOR_FALSE)): - return ("bool", bool(value.val)) + return (CBOR_KeyKind.BOOL, bool(value.val)) if isinstance(value, CBOR_NULL): - return ("null", None) + return (CBOR_KeyKind.NULL, None) if isinstance(value, CBOR_UNDEFINED): - return ("undef", None) + return (CBOR_KeyKind.UNDEF, None) if isinstance(value, (CBOR_UNSIGNED_INTEGER, CBOR_NEGATIVE_INTEGER)): - return ("int", int(value.val)) + return (CBOR_KeyKind.INT, int(value.val)) if isinstance(value, CBOR_BYTE_STRING): - return ("bstr", bytes(value.val)) + return (CBOR_KeyKind.BSTR, bytes(value.val)) if isinstance(value, CBOR_TEXT_STRING): - return ("tstr", str(value.val)) + return (CBOR_KeyKind.TSTR, str(value.val)) if isinstance(value, CBOR_FLOAT): return _cbor_float_key_identity( value.val, getattr(value, "_encoded", None) ) if isinstance(value, CBOR_ARRAY): - return ("array", tuple(_cbor_key_norm(v) for v in value.val)) + return ( + CBOR_KeyKind.ARRAY, + tuple(_cbor_key_norm(v) for v in value.val), + ) if isinstance(value, CBOR_MAP): return ( - "map", + CBOR_KeyKind.MAP, frozenset( (_cbor_key_norm(k), _cbor_key_norm(v)) for k, v in _cbor_map_pairs(value) @@ -864,13 +923,17 @@ def _cbor_key_norm(value): ) if isinstance(value, CBOR_SEMANTIC_TAG): tag_num, inner = value.val - return ("tag", int(tag_num), _cbor_key_norm(inner)) + return (CBOR_KeyKind.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)) + return (CBOR_KeyKind.SIMPLE, int(value.val)) + return ( + CBOR_KeyKind.OBJ, + type(value).__name__, + _cbor_key_norm(value.val), + ) if isinstance(value, CBORMapData): return ( - "map", + CBOR_KeyKind.MAP, frozenset( (_cbor_key_norm(k), _cbor_key_norm(v)) for k, v in value.cbor_pairs() @@ -878,28 +941,28 @@ def _cbor_key_norm(value): ) if isinstance(value, dict): return ( - "map", + CBOR_KeyKind.MAP, frozenset( (_cbor_key_norm(k), _cbor_key_norm(v)) for k, v in value.items() ), ) if isinstance(value, bool): - return ("bool", value) + return (CBOR_KeyKind.BOOL, value) if isinstance(value, int): - return ("int", value) + return (CBOR_KeyKind.INT, value) if isinstance(value, float): return _cbor_float_key_identity(value) if isinstance(value, bytes): - return ("bstr", value) + return (CBOR_KeyKind.BSTR, value) if isinstance(value, str): - return ("tstr", value) + return (CBOR_KeyKind.TSTR, value) if isinstance(value, list): - return ("array", tuple(_cbor_key_norm(v) for v in value)) + return (CBOR_KeyKind.ARRAY, tuple(_cbor_key_norm(v) for v in value)) if isinstance(value, tuple) and len(value) == 2 and isinstance(value[0], int): # Bare semantic-tag tuple (tag_num, inner), as stored on CBOR_SEMANTIC_TAG. - return ("tag", int(value[0]), _cbor_key_norm(value[1])) - return ("other", type(value).__name__, repr(value)) + return (CBOR_KeyKind.TAG, int(value[0]), _cbor_key_norm(value[1])) + return (CBOR_KeyKind.OTHER, type(value).__name__, repr(value)) def _cbor_key_equivalent(a, b): diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py index c13360e5bbe..a86e02bf82c 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -12,6 +12,7 @@ Any, Dict, Generic, + Iterable, List, Optional, Set, @@ -22,6 +23,7 @@ cast, ) +from scapy.utils import EnumElement from scapy.cbor.cbor import ( CBOR_AdditionalInfo, CBOR_Codecs, @@ -35,6 +37,7 @@ CBOR_SimpleValue, CBOR_UINT64_MAX, _CBOR_ERROR, + _CBORKeyNorm, ) from scapy.compat import chb from scapy.error import log_runtime @@ -42,6 +45,12 @@ MAX_CBOR_NESTING = 128 +# Wire buffers accepted by decode helpers (slicing preserves the concrete type). +_CBORBuf = Union[bytes, bytearray, memoryview] +_CBORBufT = TypeVar("_CBORBufT", bytes, bytearray, memoryview) +# Major-type / AI arguments: plain ints or Scapy ``EnumElement`` members. +_CBORHeadInt = Union[int, EnumElement] + ################## # CBOR encoding # @@ -81,13 +90,13 @@ def __init__(self, def CBOR_encode_initial(major_type, additional_info): - # type: (Any, Any) -> bytes + # type: (_CBORHeadInt, _CBORHeadInt) -> bytes """Encode a CBOR initial byte (3-bit major type + 5-bit additional info).""" return chb((int(major_type) << 5) | int(additional_info)) def CBOR_encode_head(major_type, value): - # type: (Any, int) -> bytes + # type: (_CBORHeadInt, int) -> bytes """ Encode CBOR initial byte and additional info for a definite argument. Format: 3 bits major type + 5 bits additional info @@ -103,7 +112,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 < int(CBOR_AdditionalInfo.ONE_BYTE): + if value < 24: # Value fits in 5 bits return CBOR_encode_initial(major_type, value) elif value < 256: @@ -136,23 +145,21 @@ def CBOR_encode_head(major_type, value): def _cbor_buf_bytes(buf): - # type: (Any) -> bytes + # type: (_CBORBuf) -> 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 + # type: (_CBORBuf) -> bool """Return whether *s* begins with a CBOR break byte.""" return bool(s) and s[0] == CBOR_BREAK_BYTE def cbor_consume_break(s): - # type: (Any) -> Any + # type: (_CBORBufT) -> _CBORBufT """Consume a leading CBOR break byte from *s*.""" if not cbor_is_break(s): raise CBOR_Codec_Decoding_Error( @@ -161,7 +168,7 @@ def cbor_consume_break(s): def _cbor_skip_item(s, depth=0): - # type: (Any, int) -> Any + # type: (_CBORBufT, int) -> _CBORBufT """Advance past one well-formed CBOR item without building objects.""" if depth > MAX_CBOR_NESTING: raise CBOR_Codec_Decoding_Error( @@ -169,14 +176,14 @@ def _cbor_skip_item(s, depth=0): 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), + CBOR_MajorTypes.UNSIGNED_INTEGER, + CBOR_MajorTypes.NEGATIVE_INTEGER, + CBOR_MajorTypes.SIMPLE_AND_FLOAT, ): return rem if major_type in ( - int(CBOR_MajorTypes.BYTE_STRING), - int(CBOR_MajorTypes.TEXT_STRING), + CBOR_MajorTypes.BYTE_STRING, + CBOR_MajorTypes.TEXT_STRING, ): if value is CBOR_INDEFINITE: expected_type = major_type @@ -204,7 +211,7 @@ def _cbor_skip_item(s, depth=0): "Truncated byte/text string", remaining=_cbor_buf_bytes(rem)) return rem[length:] - if major_type == int(CBOR_MajorTypes.ARRAY): + if major_type == CBOR_MajorTypes.ARRAY: if value is CBOR_INDEFINITE: while rem and not cbor_is_break(rem): rem = _cbor_skip_item(rem, depth + 1) @@ -212,7 +219,7 @@ def _cbor_skip_item(s, depth=0): for _ in range(int(value)): rem = _cbor_skip_item(rem, depth + 1) return rem - if major_type == int(CBOR_MajorTypes.MAP): + if major_type == CBOR_MajorTypes.MAP: if value is CBOR_INDEFINITE: while rem and not cbor_is_break(rem): rem = _cbor_skip_item(rem, depth + 1) @@ -222,7 +229,7 @@ def _cbor_skip_item(s, depth=0): rem = _cbor_skip_item(rem, depth + 1) rem = _cbor_skip_item(rem, depth + 1) return rem - if major_type == int(CBOR_MajorTypes.TAG): + if major_type == CBOR_MajorTypes.TAG: return _cbor_skip_item(rem, depth + 1) raise CBOR_Codec_Decoding_Error( "Invalid major type: %d" % major_type, @@ -231,7 +238,7 @@ def _cbor_skip_item(s, depth=0): def cbor_count_items(s, max_count=None, until_break=False): - # type: (Any, Optional[int], bool) -> int + # type: (_CBORBuf, Optional[int], bool) -> int """Count top-level CBOR items without building object trees. When *until_break* is true, stop at a break byte without consuming it. @@ -248,7 +255,7 @@ def cbor_count_items(s, max_count=None, until_break=False): def cbor_item_span(s): - # type: (Any) -> Tuple[bytes, bytes] + # type: (_CBORBuf) -> Tuple[bytes, bytes] """Split *s* into the first structural CBOR item and the remainder. Uses :func:`_cbor_skip_item` for boundary finding only. Callers that must @@ -262,7 +269,7 @@ def cbor_item_span(s): def CBOR_decode_head(s): - # type: (Any) -> Tuple[int, Union[int, CBOR_INDEFINITE], Any] + # type: (_CBORBuf) -> Tuple[int, Union[int, CBOR_INDEFINITE], _CBORBuf] """ Decode CBOR initial byte and additional info. Returns: (major_type, value, remaining_bytes) @@ -275,17 +282,18 @@ def CBOR_decode_head(s): major_type = initial_byte >> 5 additional_info = initial_byte & 0x1f - if additional_info < int(CBOR_AdditionalInfo.ONE_BYTE): - # Value is in the additional info + # 0-23: argument is the additional-info nibble itself + # (CBOR_AdditionalInfo.ONE_BYTE == 24). + if additional_info < 24: return major_type, additional_info, s[1:] - elif additional_info == int(CBOR_AdditionalInfo.ONE_BYTE): + elif additional_info == CBOR_AdditionalInfo.ONE_BYTE: # 1-byte value follows if len(s) < 2: raise CBOR_Codec_Decoding_Error( "Not enough bytes for 1-byte value", remaining=_cbor_buf_bytes(s)) return major_type, s[1], s[2:] - elif additional_info == int(CBOR_AdditionalInfo.TWO_BYTES): + elif additional_info == CBOR_AdditionalInfo.TWO_BYTES: # 2-byte value follows if len(s) < 3: raise CBOR_Codec_Decoding_Error( @@ -293,7 +301,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 == int(CBOR_AdditionalInfo.FOUR_BYTES): + elif additional_info == CBOR_AdditionalInfo.FOUR_BYTES: # 4-byte value follows if len(s) < 5: raise CBOR_Codec_Decoding_Error( @@ -301,7 +309,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 == int(CBOR_AdditionalInfo.EIGHT_BYTES): + elif additional_info == CBOR_AdditionalInfo.EIGHT_BYTES: # 8-byte value follows if len(s) < 9: raise CBOR_Codec_Decoding_Error( @@ -309,29 +317,29 @@ 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 == int(CBOR_AdditionalInfo.INDEFINITE): + elif additional_info == CBOR_AdditionalInfo.INDEFINITE: if major_type in ( - int(CBOR_MajorTypes.UNSIGNED_INTEGER), - int(CBOR_MajorTypes.NEGATIVE_INTEGER), - int(CBOR_MajorTypes.TAG), + CBOR_MajorTypes.UNSIGNED_INTEGER, + CBOR_MajorTypes.NEGATIVE_INTEGER, + CBOR_MajorTypes.TAG, ): raise CBOR_Codec_Decoding_Error( "Indefinite length not allowed for major type %d" % major_type, remaining=_cbor_buf_bytes(s)) if major_type in ( - int(CBOR_MajorTypes.BYTE_STRING), - int(CBOR_MajorTypes.TEXT_STRING), - int(CBOR_MajorTypes.ARRAY), - int(CBOR_MajorTypes.MAP), + CBOR_MajorTypes.BYTE_STRING, + CBOR_MajorTypes.TEXT_STRING, + CBOR_MajorTypes.ARRAY, + CBOR_MajorTypes.MAP, ): return major_type, CBOR_INDEFINITE, s[1:] raise CBOR_Codec_Decoding_Error( "Indefinite length not allowed for major type %d" % major_type, remaining=_cbor_buf_bytes(s)) elif additional_info in ( - int(CBOR_AdditionalInfo.RESERVED_28), - int(CBOR_AdditionalInfo.RESERVED_29), - int(CBOR_AdditionalInfo.RESERVED_30), + CBOR_AdditionalInfo.RESERVED_28, + CBOR_AdditionalInfo.RESERVED_29, + CBOR_AdditionalInfo.RESERVED_30, ): raise CBOR_Codec_Decoding_Error( "Reserved additional info: %d" % additional_info, @@ -344,20 +352,9 @@ def CBOR_decode_head(s): def _cbor_float_from_bits(ai, bits): # type: (int, int) -> float - if ai == int(CBOR_FloatAI.HALF): - 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 == int(CBOR_FloatAI.SINGLE): + if ai == CBOR_FloatAI.HALF: + return struct.unpack(">e", struct.pack(">H", bits & 0xffff))[0] + if ai == CBOR_FloatAI.SINGLE: return struct.unpack(">f", struct.pack(">I", bits))[0] return struct.unpack(">d", struct.pack(">Q", bits))[0] @@ -372,34 +369,17 @@ def _cbor_float_to_half_bits(value): 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: + try: + packed = struct.pack(">e", value) + except (OverflowError, ValueError): 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 - bits = sign | half - 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 - half_exp = exp64 + 15 - half_mant = mant64 >> 42 - # Reject if discarded mantissa bits are nonzero (not exact). - if mant64 & ((1 << 42) - 1): + bits = struct.unpack(">H", packed)[0] + decoded = struct.unpack(">e", packed)[0] + if math.isinf(decoded) or decoded != value: return None - bits = sign | (half_exp << 10) | half_mant - decoded = _cbor_float_from_bits(int(CBOR_FloatAI.HALF), bits) - if decoded != math.copysign(abs(value), -1.0 if sign else 1.0): + if value == 0.0 and ( + math.copysign(1.0, decoded) != math.copysign(1.0, value) + ): return None return bits @@ -411,15 +391,16 @@ 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_FloatAI.HALF): + # Return plain ints: callers order preferred AI with ``<``. + if ai == CBOR_FloatAI.HALF: return int(CBOR_FloatAI.HALF) - if ai == int(CBOR_FloatAI.SINGLE): + if ai == CBOR_FloatAI.SINGLE: # binary32 NaN: 1+8+23. Prefer half when low 13 significand bits are 0. mant = int(bits) & 0x7FFFFF if mant and (mant & ((1 << 13) - 1)) == 0: return int(CBOR_FloatAI.HALF) return int(CBOR_FloatAI.SINGLE) - if ai == int(CBOR_FloatAI.DOUBLE): + if ai == CBOR_FloatAI.DOUBLE: # binary64 NaN: 1+11+52. mant = int(bits) & ((1 << 52) - 1) if mant == 0: @@ -441,22 +422,25 @@ 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. + + Bit extraction is required here: ``struct`` float formats discard NaN + payloads, which RFC 8949 map-key identity must preserve. """ - if ai == int(CBOR_FloatAI.HALF): + if ai == CBOR_FloatAI.HALF: sign = (int(bits) >> 15) & 0x1 exponent = (int(bits) >> 10) & 0x1f fraction = int(bits) & 0x3ff if exponent != 31 or not fraction: return None return sign, fraction << 42 - if ai == int(CBOR_FloatAI.SINGLE): + if ai == CBOR_FloatAI.SINGLE: sign = (int(bits) >> 31) & 0x1 exponent = (int(bits) >> 23) & 0xff fraction = int(bits) & 0x7fffff if exponent != 0xff or not fraction: return None return sign, fraction << 29 - if ai == int(CBOR_FloatAI.DOUBLE): + if ai == CBOR_FloatAI.DOUBLE: sign = (int(bits) >> 63) & 0x1 exponent = (int(bits) >> 52) & 0x7ff fraction = int(bits) & ((1 << 52) - 1) @@ -470,6 +454,7 @@ def _cbor_preferred_float_ai(value): # type: (float) -> int """Return the preferred float AI for a numeric *value*.""" import math + # Return plain ints: callers order preferred AI with ``<``. if math.isnan(value): # Without the original payload bits, only the quiet binary16 NaN is a # safe generic preference. Encoded-width checks use bit patterns. @@ -608,10 +593,10 @@ def encode_cbor_item(item): @staticmethod def _reject_duplicate_map_keys(pairs): - # type: (Any) -> None + # type: (Iterable[Tuple[Any, Any]]) -> None """Raise if *pairs* contain CBOR-equivalent duplicate keys.""" from scapy.cbor.cbor import _cbor_key_norm - seen_norms = set() # type: Set[Any] + seen_norms = set() # type: Set[_CBORKeyNorm] for key, _value in pairs: norm = _cbor_key_norm(key) if norm in seen_norms: @@ -622,7 +607,7 @@ def _reject_duplicate_map_keys(pairs): @staticmethod def _encode_cbor_map_deterministic(pairs): - # type: (Any) -> bytes + # type: (Iterable[Tuple[Any, Any]]) -> bytes """Encode map pairs in RFC 8949 core-deterministic key order.""" pairs = list(pairs) CBORcodec_Object._reject_duplicate_map_keys(pairs) @@ -684,7 +669,7 @@ def encode_cbor_item_deterministic(item): ) sign, significand52 = comps preferred = _cbor_nan_preferred_ai(ai, bits) - if preferred == int(CBOR_FloatAI.HALF): + if preferred == CBOR_FloatAI.HALF: fraction = (significand52 >> 42) & 0x3ff nan_bits = (sign << 15) | (0x1f << 10) | fraction return ( @@ -693,7 +678,7 @@ def encode_cbor_item_deterministic(item): CBOR_FloatAI.HALF) + struct.pack(">H", nan_bits) ) - if preferred == int(CBOR_FloatAI.SINGLE): + if preferred == CBOR_FloatAI.SINGLE: fraction = (significand52 >> 29) & 0x7fffff nan_bits = (sign << 31) | (0xff << 23) | fraction return ( @@ -702,7 +687,7 @@ def encode_cbor_item_deterministic(item): CBOR_FloatAI.SINGLE) + struct.pack(">I", nan_bits) ) - if preferred == int(CBOR_FloatAI.DOUBLE): + if preferred == CBOR_FloatAI.DOUBLE: nan_bits = ( (sign << 63) | (0x7ff << 52) | @@ -756,7 +741,7 @@ def encode_cbor_item_deterministic(item): @staticmethod def decode_cbor_item(s, depth=0): - # type: (Any, int) -> Tuple[CBOR_Object[Any], Any] + # type: (_CBORBuf, int) -> Tuple[CBOR_Object[Any], _CBORBuf] """Decode CBOR bytes to a CBOR_Object. Top-level callers may pass ``bytes`` (or a subclass). Decoding then @@ -787,21 +772,21 @@ def decode_cbor_item(s, depth=0): major_type = initial_byte >> 5 # Dispatch to appropriate codec based on major type - if major_type == int(CBOR_MajorTypes.UNSIGNED_INTEGER): + if major_type == CBOR_MajorTypes.UNSIGNED_INTEGER: return CBORcodec_UNSIGNED_INTEGER.dec(s, safe=False, _depth=depth) - elif major_type == int(CBOR_MajorTypes.NEGATIVE_INTEGER): + elif major_type == CBOR_MajorTypes.NEGATIVE_INTEGER: return CBORcodec_NEGATIVE_INTEGER.dec(s, safe=False, _depth=depth) - elif major_type == int(CBOR_MajorTypes.BYTE_STRING): + elif major_type == CBOR_MajorTypes.BYTE_STRING: return CBORcodec_BYTE_STRING.dec(s, safe=False, _depth=depth) - elif major_type == int(CBOR_MajorTypes.TEXT_STRING): + elif major_type == CBOR_MajorTypes.TEXT_STRING: return CBORcodec_TEXT_STRING.dec(s, safe=False, _depth=depth) - elif major_type == int(CBOR_MajorTypes.ARRAY): + elif major_type == CBOR_MajorTypes.ARRAY: return CBORcodec_ARRAY.dec(s, safe=False, _depth=depth) - elif major_type == int(CBOR_MajorTypes.MAP): + elif major_type == CBOR_MajorTypes.MAP: return CBORcodec_MAP.dec(s, safe=False, _depth=depth) - elif major_type == int(CBOR_MajorTypes.TAG): + elif major_type == CBOR_MajorTypes.TAG: return CBORcodec_SEMANTIC_TAG.dec(s, safe=False, _depth=depth) - elif major_type == int(CBOR_MajorTypes.SIMPLE_AND_FLOAT): + elif major_type == CBOR_MajorTypes.SIMPLE_AND_FLOAT: return CBORcodec_SIMPLE_AND_FLOAT.dec(s, safe=False, _depth=depth) else: raise CBOR_Codec_Decoding_Error( @@ -845,7 +830,7 @@ def do_dec(cls, # type: (...) -> Tuple[CBOR_Object[int], bytes] cls.check_string(s) major_type, value, remainder = CBOR_decode_head(s) - if major_type != int(CBOR_MajorTypes.UNSIGNED_INTEGER): + if major_type != CBOR_MajorTypes.UNSIGNED_INTEGER: raise CBOR_Codec_Decoding_Error( "Expected major type 0 (unsigned integer), got %d" % major_type, remaining=s) @@ -881,7 +866,7 @@ def do_dec(cls, # type: (...) -> Tuple[CBOR_Object[int], bytes] cls.check_string(s) major_type, value, remainder = CBOR_decode_head(s) - if major_type != int(CBOR_MajorTypes.NEGATIVE_INTEGER): + if major_type != CBOR_MajorTypes.NEGATIVE_INTEGER: raise CBOR_Codec_Decoding_Error( "Expected major type 1 (negative integer), got %d" % major_type, remaining=s) @@ -912,7 +897,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 != int(CBOR_MajorTypes.BYTE_STRING): + if major_type != CBOR_MajorTypes.BYTE_STRING: raise CBOR_Codec_Decoding_Error( "Expected major type 2 (byte string), got %d" % major_type, remaining=s) @@ -923,7 +908,7 @@ def do_dec(cls, remainder = cbor_consume_break(remainder) break chunk_mt, chunk_len, remainder = CBOR_decode_head(remainder) - if chunk_mt != int(CBOR_MajorTypes.BYTE_STRING): + if chunk_mt != CBOR_MajorTypes.BYTE_STRING: raise CBOR_Codec_Decoding_Error( "Indefinite byte string chunk must be major type 2", remaining=remainder) @@ -976,7 +961,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 != int(CBOR_MajorTypes.TEXT_STRING): + if major_type != CBOR_MajorTypes.TEXT_STRING: raise CBOR_Codec_Decoding_Error( "Expected major type 3 (text string), got %d" % major_type, remaining=s) @@ -987,7 +972,7 @@ def do_dec(cls, remainder = cbor_consume_break(remainder) break chunk_mt, chunk_len, remainder = CBOR_decode_head(remainder) - if chunk_mt != int(CBOR_MajorTypes.TEXT_STRING): + if chunk_mt != CBOR_MajorTypes.TEXT_STRING: raise CBOR_Codec_Decoding_Error( "Indefinite text string chunk must be major type 3", remaining=remainder) @@ -1047,7 +1032,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 != int(CBOR_MajorTypes.ARRAY): + if major_type != CBOR_MajorTypes.ARRAY: raise CBOR_Codec_Decoding_Error( "Expected major type 4 (array), got %d" % major_type, remaining=s) @@ -1109,13 +1094,13 @@ 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 != int(CBOR_MajorTypes.MAP): + if major_type != CBOR_MajorTypes.MAP: raise CBOR_Codec_Decoding_Error( "Expected major type 5 (map), got %d" % major_type, remaining=s) pairs = [] # type: List[Tuple[Any, Any]] - seen_norms = set() # type: Set[Any] + seen_norms = set() # type: Set[_CBORKeyNorm] def _add_pair(key, value): # type: (Any, Any) -> None @@ -1189,7 +1174,7 @@ def do_dec(cls, # type: (...) -> Tuple[CBOR_Object[Tuple[int, Any]], bytes] cls.check_string(s) major_type, tag_num, remainder = CBOR_decode_head(s) - if major_type != int(CBOR_MajorTypes.TAG): + if major_type != CBOR_MajorTypes.TAG: raise CBOR_Codec_Decoding_Error( "Expected major type 6 (tag), got %d" % major_type, remaining=s) @@ -1247,7 +1232,7 @@ 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_FloatAI.HALF): + if ai == CBOR_FloatAI.HALF: half = _cbor_float_to_half_bits(val) if half is not None: return ( @@ -1255,8 +1240,8 @@ def enc(cls, obj): CBOR_FloatAI.HALF) + struct.pack(">H", half) ) - ai = int(CBOR_FloatAI.SINGLE) - if ai == int(CBOR_FloatAI.SINGLE): + ai = CBOR_FloatAI.SINGLE + if ai == CBOR_FloatAI.SINGLE: try: return ( CBOR_encode_initial(CBOR_MajorTypes.SIMPLE_AND_FLOAT, @@ -1298,35 +1283,34 @@ def do_dec(cls, cls.check_string(s) - # For major type 7, we need special handling because additional_info - # encodes different things (simple values vs float sizes) + # For major type 7, additional_info encodes simple values vs float sizes. initial_byte = s[0] major_type = initial_byte >> 5 additional_info = initial_byte & 0x1f - if major_type != int(CBOR_MajorTypes.SIMPLE_AND_FLOAT): + if major_type != CBOR_MajorTypes.SIMPLE_AND_FLOAT: raise CBOR_Codec_Decoding_Error( "Expected major type 7 (simple/float), got %d" % major_type, remaining=s) # Check for special simple values (encoded directly in additional_info) - if additional_info == int(CBOR_SimpleValue.FALSE): + if additional_info == CBOR_SimpleValue.FALSE: return CBOR_FALSE(), s[1:] - elif additional_info == int(CBOR_SimpleValue.TRUE): + elif additional_info == CBOR_SimpleValue.TRUE: return CBOR_TRUE(), s[1:] - elif additional_info == int(CBOR_SimpleValue.NULL): + elif additional_info == CBOR_SimpleValue.NULL: return CBOR_NULL(), s[1:] - elif additional_info == int(CBOR_SimpleValue.UNDEFINED): + elif additional_info == CBOR_SimpleValue.UNDEFINED: return CBOR_UNDEFINED(), s[1:] elif additional_info in ( - int(CBOR_FloatAI.HALF), - int(CBOR_FloatAI.SINGLE), - int(CBOR_FloatAI.DOUBLE), + CBOR_FloatAI.HALF, + CBOR_FloatAI.SINGLE, + CBOR_FloatAI.DOUBLE, ): width = { - int(CBOR_FloatAI.HALF): 2, - int(CBOR_FloatAI.SINGLE): 4, - int(CBOR_FloatAI.DOUBLE): 8, + CBOR_FloatAI.HALF: 2, + CBOR_FloatAI.SINGLE: 4, + CBOR_FloatAI.DOUBLE: 8, }[additional_info] if len(s) < 1 + width: raise CBOR_Codec_Decoding_Error( @@ -1336,12 +1320,12 @@ 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 < int(CBOR_AdditionalInfo.ONE_BYTE): - # Simple value 0-23 + elif additional_info < 24: + # Simple value 0-23 (below CBOR_AdditionalInfo.ONE_BYTE) return CBOR_SIMPLE_VALUE(additional_info), s[1:] else: # additional_info 24 means 1-byte simple value follows - if additional_info == int(CBOR_AdditionalInfo.ONE_BYTE): + if additional_info == 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 a82cd8f4a60..fcee3ad09ac 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -22,6 +22,7 @@ CBOR_AdditionalInfo, CBOR_Decoding_Error, CBOR_Encoding_Error, + CBOR_FingerprintKind, CBOR_FloatAI, CBOR_MajorTypes, CBOR_Object, @@ -41,6 +42,7 @@ CBOR_FLOAT, CBOR_MAP, CBOR_SIMPLE_VALUE, + _CBORFingerprint, ) from scapy.cbor.cborcodec import ( CBOR_BREAK_BYTE, @@ -495,36 +497,37 @@ def python_to_cbor_object(value): @staticmethod def _cache_fingerprint(obj): - # type: (Any) -> Any + # type: (Any) -> _CBORFingerprint """Recursive rebuild-relevant fingerprint for ``CBORF_ANY`` values.""" from scapy.cbor.cbor import CBORMapData fingerprint = CBORF_ANY._cache_fingerprint + Kind = CBOR_FingerprintKind if obj is CBOR_ABSENT or obj is CBOR_NO_ITEM: - return ("sentinel", obj) + return (Kind.SENTINEL, obj) if isinstance(obj, CBOR_UNDEFINED): - return ("undefined",) + return (Kind.UNDEF,) if isinstance(obj, CBOR_FLOAT): fval = float(obj.val) if math.isnan(fval): - token = ("nan",) # type: Any + token = (Kind.NAN,) # type: Tuple[Any, ...] elif math.isinf(fval): - token = ("inf", math.copysign(1.0, fval)) + token = (Kind.INF, math.copysign(1.0, fval)) elif fval == 0.0: - token = ("zero", math.copysign(1.0, fval)) + token = (Kind.ZERO, math.copysign(1.0, fval)) else: - token = ("num", fval) + token = (Kind.FINITE, fval) encoded = getattr(obj, "_encoded", None) - return ("float", token, encoded) + return (Kind.FLOAT, token, encoded) if isinstance(obj, CBOR_ARRAY): return ( - "array", + Kind.ARRAY, tuple(fingerprint(item) for item in obj.val), ) if isinstance(obj, CBOR_MAP): from scapy.cbor.cbor import _cbor_map_pairs pairs = _cbor_map_pairs(obj) return ( - "map", + Kind.MAP, tuple( (fingerprint(key), fingerprint(value)) for key, value in pairs @@ -532,7 +535,7 @@ def _cache_fingerprint(obj): ) if isinstance(obj, CBORMapData): return ( - "mapdata", + Kind.MAPDATA, tuple( (fingerprint(key), fingerprint(value)) for key, value in obj.cbor_pairs() @@ -540,23 +543,23 @@ def _cache_fingerprint(obj): ) if isinstance(obj, CBOR_SEMANTIC_TAG): tag_num, inner = obj.val - return ("tag", int(tag_num), fingerprint(inner)) + return (Kind.TAG, int(tag_num), fingerprint(inner)) if isinstance(obj, CBOR_Object): - return (type(obj).__name__, obj.val) + return (Kind.OBJ, type(obj).__name__, obj.val) if isinstance(obj, list): - return ("list", tuple(fingerprint(item) for item in obj)) + return (Kind.LIST, tuple(fingerprint(item) for item in obj)) if isinstance(obj, dict): return ( - "dict", + Kind.DICT, tuple( (fingerprint(key), fingerprint(value)) for key, value in obj.items() ), ) - return ("py", type(obj).__name__, obj) + return (Kind.PY, type(obj).__name__, obj) def cache_fingerprint(self, x): - # type: (Any) -> Any + # type: (Any) -> _CBORFingerprint """Snapshot for Scapy mutable raw-cache comparison. Includes ``CBOR_FLOAT._encoded`` so explicit ``.val`` assignment that @@ -680,8 +683,8 @@ def matches_next_item(self, pkt, s): except CBOR_Codec_Decoding_Error: return False return major_type in ( - int(CBOR_MajorTypes.UNSIGNED_INTEGER), - int(CBOR_MajorTypes.NEGATIVE_INTEGER), + CBOR_MajorTypes.UNSIGNED_INTEGER, + CBOR_MajorTypes.NEGATIVE_INTEGER, ) def any2i(self, pkt, x): @@ -701,10 +704,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 == int(CBOR_MajorTypes.UNSIGNED_INTEGER): + if major_type == CBOR_MajorTypes.UNSIGNED_INTEGER: obj, remain = CBORcodec_UNSIGNED_INTEGER.dec(s) return obj.val, remain - elif major_type == int(CBOR_MajorTypes.NEGATIVE_INTEGER): + elif major_type == CBOR_MajorTypes.NEGATIVE_INTEGER: obj, remain = CBORcodec_NEGATIVE_INTEGER.dec(s) return obj.val, remain raise CBOR_Type_Mismatch( @@ -730,7 +733,7 @@ def _cbor_decode_byte_string(s, definite_only=False): major_type, length, _rem = CBOR_decode_head(s) except CBOR_Codec_Decoding_Error as e: raise CBOR_Decoding_Error(str(e)) - if major_type != int(CBOR_MajorTypes.BYTE_STRING): + if major_type != CBOR_MajorTypes.BYTE_STRING: raise CBOR_Type_Mismatch( "Expected byte string, got major type %d" % major_type) if length is CBOR_INDEFINITE: @@ -894,10 +897,10 @@ def matches_next_item(self, pkt, s): return False ai = s[0] & 0x1f return ( - ((s[0] >> 5) & 0x7) == int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) + ((s[0] >> 5) & 0x7) == CBOR_MajorTypes.SIMPLE_AND_FLOAT and ai in ( - int(CBOR_SimpleValue.FALSE), - int(CBOR_SimpleValue.TRUE), + CBOR_SimpleValue.FALSE, + CBOR_SimpleValue.TRUE, ) ) @@ -1055,11 +1058,11 @@ def matches_next_item(self, pkt, s): return False ai = s[0] & 0x1f return ( - ((s[0] >> 5) & 0x7) == int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) + ((s[0] >> 5) & 0x7) == CBOR_MajorTypes.SIMPLE_AND_FLOAT and ai in ( - int(CBOR_FloatAI.HALF), - int(CBOR_FloatAI.SINGLE), - int(CBOR_FloatAI.DOUBLE), + CBOR_FloatAI.HALF, + CBOR_FloatAI.SINGLE, + CBOR_FloatAI.DOUBLE, ) ) @@ -1308,7 +1311,7 @@ def _dissect_counted(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 != int(CBOR_MajorTypes.ARRAY): + if major_type != CBOR_MajorTypes.ARRAY: raise CBOR_Type_Mismatch( "Expected major type 4 (array), got %d" % major_type) if count is CBOR_INDEFINITE: @@ -1656,7 +1659,7 @@ 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 != int(CBOR_MajorTypes.ARRAY): + if major_type != CBOR_MajorTypes.ARRAY: raise CBOR_Type_Mismatch( "Expected major type 4 (array), got %d" % major_type) lst = [] # type: List[Any] @@ -1711,7 +1714,7 @@ def do_copy(self, x): # type: ignore[override] return copy.deepcopy(x) def cache_fingerprint(self, x): - # type: (Any) -> Any + # type: (Any) -> _CBORFingerprint """Wire-sensitive fingerprint for unknown text-key extension pairs.""" if not x: return () @@ -1863,7 +1866,7 @@ def _dissect_counted(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 != int(CBOR_MajorTypes.MAP): + if major_type != CBOR_MajorTypes.MAP: raise CBOR_Type_Mismatch( "Expected major type 5 (map), got %d" % major_type) @@ -2011,7 +2014,7 @@ def _parse_tag_head(self, 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 != int(CBOR_MajorTypes.TAG): + if major_type != CBOR_MajorTypes.TAG: raise CBOR_Type_Mismatch( "Expected major type 6 (semantic tag), got %d" % major_type) if tag_num != self.tag_num: @@ -2032,7 +2035,7 @@ def matches_next_item(self, pkt, s): except CBOR_Codec_Decoding_Error: return False return ( - major_type == int(CBOR_MajorTypes.TAG) + major_type == CBOR_MajorTypes.TAG and tag_num == self.tag_num ) diff --git a/scapy/utils.py b/scapy/utils.py index 2e0e0479075..ba9e439d7e3 100644 --- a/scapy/utils.py +++ b/scapy/utils.py @@ -1186,6 +1186,14 @@ def __neq__(self, other): # type: (Any) -> bool return not self.__eq__(other) + def __copy__(self): + # type: () -> EnumElement + return self + + def __deepcopy__(self, memo): + # type: (Dict[Any, Any]) -> EnumElement + return self + class Enum_metaclass(type): element_class = EnumElement diff --git a/test/scapy/layers/cbor_test_utils.py b/test/scapy/layers/cbor_test_utils.py index ae6f56d6c4a..a4a24df85f6 100644 --- a/test/scapy/layers/cbor_test_utils.py +++ b/test/scapy/layers/cbor_test_utils.py @@ -40,18 +40,18 @@ def cbor_find_non_deterministic(s, allow_indefinite=False, base_offset=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 ai == CBOR_AdditionalInfo.INDEFINITE + if ai < 24: return True - if ai == int(CBOR_AdditionalInfo.ONE_BYTE): - return int(value) >= int(CBOR_AdditionalInfo.ONE_BYTE) - if ai == int(CBOR_AdditionalInfo.TWO_BYTES): + if ai == CBOR_AdditionalInfo.ONE_BYTE: + return int(value) >= 24 + if ai == CBOR_AdditionalInfo.TWO_BYTES: return int(value) >= 256 - if ai == int(CBOR_AdditionalInfo.FOUR_BYTES): + if ai == CBOR_AdditionalInfo.FOUR_BYTES: return int(value) >= 65536 - if ai == int(CBOR_AdditionalInfo.EIGHT_BYTES): + if ai == CBOR_AdditionalInfo.EIGHT_BYTES: return int(value) >= (1 << 32) - return ai == int(CBOR_AdditionalInfo.INDEFINITE) + return ai == CBOR_AdditionalInfo.INDEFINITE def _walk(depth=0): # type: (int) -> None @@ -74,38 +74,38 @@ def _walk(depth=0): major = initial >> 5 ai = initial & 0x1f pos = start + 1 - if ai < int(CBOR_AdditionalInfo.ONE_BYTE): + if ai < 24: value = ai # type: Union[int, CBOR_INDEFINITE] - elif ai == int(CBOR_AdditionalInfo.ONE_BYTE): + elif ai == CBOR_AdditionalInfo.ONE_BYTE: if pos + 1 > len(s): raise CBOR_Codec_Decoding_Error( "Not enough bytes for 1-byte value", remaining=s[start:]) value = s[pos] pos += 1 - elif ai == int(CBOR_AdditionalInfo.TWO_BYTES): + elif ai == CBOR_AdditionalInfo.TWO_BYTES: if pos + 2 > len(s): raise CBOR_Codec_Decoding_Error( "Not enough bytes for 2-byte value", remaining=s[start:]) value = struct.unpack(">H", s[pos:pos + 2])[0] pos += 2 - elif ai == int(CBOR_AdditionalInfo.FOUR_BYTES): + elif ai == CBOR_AdditionalInfo.FOUR_BYTES: if pos + 4 > len(s): raise CBOR_Codec_Decoding_Error( "Not enough bytes for 4-byte value", remaining=s[start:]) value = struct.unpack(">I", s[pos:pos + 4])[0] pos += 4 - elif ai == int(CBOR_AdditionalInfo.EIGHT_BYTES): + elif ai == CBOR_AdditionalInfo.EIGHT_BYTES: if pos + 8 > len(s): raise CBOR_Codec_Decoding_Error( "Not enough bytes for 8-byte value", remaining=s[start:]) value = struct.unpack(">Q", s[pos:pos + 8])[0] pos += 8 - elif ai == int(CBOR_AdditionalInfo.INDEFINITE): + elif ai == CBOR_AdditionalInfo.INDEFINITE: value = CBOR_INDEFINITE elif ai in ( - int(CBOR_AdditionalInfo.RESERVED_28), - int(CBOR_AdditionalInfo.RESERVED_29), - int(CBOR_AdditionalInfo.RESERVED_30), + CBOR_AdditionalInfo.RESERVED_28, + CBOR_AdditionalInfo.RESERVED_29, + CBOR_AdditionalInfo.RESERVED_30, ): raise CBOR_Codec_Decoding_Error( "Reserved additional info: %d" % ai, remaining=s[start:]) @@ -115,9 +115,9 @@ def _walk(depth=0): index[0] = pos # Major type 7: simple values and floats. Check float preferred width. - if major == int(CBOR_MajorTypes.SIMPLE_AND_FLOAT): + if major == CBOR_MajorTypes.SIMPLE_AND_FLOAT: if ( - ai == int(CBOR_AdditionalInfo.ONE_BYTE) + ai == CBOR_AdditionalInfo.ONE_BYTE and isinstance(value, int) and value < 32 ): @@ -127,9 +127,9 @@ def _walk(depth=0): "(AI=24, value=%d)" % value, )) if ai in ( - int(CBOR_FloatAI.HALF), - int(CBOR_FloatAI.SINGLE), - int(CBOR_FloatAI.DOUBLE), + CBOR_FloatAI.HALF, + CBOR_FloatAI.SINGLE, + CBOR_FloatAI.DOUBLE, ) and value is not CBOR_INDEFINITE: comps = _cbor_nan_components(ai, int(value)) if comps is not None: @@ -153,8 +153,8 @@ def _walk(depth=0): "Indefinite-length item is not allowed", )) if major in ( - int(CBOR_MajorTypes.BYTE_STRING), - int(CBOR_MajorTypes.TEXT_STRING), + CBOR_MajorTypes.BYTE_STRING, + CBOR_MajorTypes.TEXT_STRING, ): while index[0] < len(s) and not cbor_is_break(s[index[0]:]): chunk_start = index[0] @@ -186,7 +186,7 @@ def _walk(depth=0): "Expected break byte (0xff)", remaining=s[index[0]:]) index[0] += 1 return - if major == int(CBOR_MajorTypes.ARRAY): + if major == CBOR_MajorTypes.ARRAY: while index[0] < len(s) and not cbor_is_break(s[index[0]:]): _walk(depth + 1) if index[0] >= len(s) or not cbor_is_break(s[index[0]:]): @@ -194,7 +194,7 @@ def _walk(depth=0): "Expected break byte (0xff)", remaining=s[index[0]:]) index[0] += 1 return - if major == int(CBOR_MajorTypes.MAP): + if major == CBOR_MajorTypes.MAP: key_encodings = [] # type: List[bytes] while index[0] < len(s) and not cbor_is_break(s[index[0]:]): key_start = index[0] @@ -224,8 +224,8 @@ def _walk(depth=0): )) if major in ( - int(CBOR_MajorTypes.BYTE_STRING), - int(CBOR_MajorTypes.TEXT_STRING), + CBOR_MajorTypes.BYTE_STRING, + CBOR_MajorTypes.TEXT_STRING, ): length = int(value) if index[0] + length > len(s): @@ -233,11 +233,11 @@ def _walk(depth=0): "Truncated byte/text string", remaining=s[start:]) index[0] += length return - if major == int(CBOR_MajorTypes.ARRAY): + if major == CBOR_MajorTypes.ARRAY: for _ in range(int(value)): _walk(depth + 1) return - if major == int(CBOR_MajorTypes.MAP): + if major == CBOR_MajorTypes.MAP: key_encodings = [] # type: List[bytes] for _ in range(int(value)): key_start = index[0] @@ -250,7 +250,7 @@ def _walk(depth=0): "CBOR map keys are not in bytewise lexicographic order", )) return - if major == int(CBOR_MajorTypes.TAG): + if major == CBOR_MajorTypes.TAG: _walk(depth + 1) return From d3777a77b0375516087be6dad6c521e792197ecb Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Fri, 11 Sep 2026 12:37:00 +0200 Subject: [PATCH 62/65] cbor: close SEQUENCE_OF holes and rename unframed fields Reject nested REMAINDER_OF, require usable CBOR_root for packet classes, snapshot generic CBOR_Object fingerprints, rename SEQUENCE/SEQUENCE_OF to ITEMS/REMAINDER_OF, and slim max_items. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/__init__.py | 8 +- scapy/cbor/cbor.py | 22 --- scapy/cbor/cborfields.py | 129 +++++++------ test/fields.uts | 30 +++ test/scapy/layers/cbor.uts | 367 ++++++++++++++++++++++--------------- 5 files changed, 323 insertions(+), 233 deletions(-) diff --git a/scapy/cbor/__init__.py b/scapy/cbor/__init__.py index 80d5e93d943..0089a3401b8 100644 --- a/scapy/cbor/__init__.py +++ b/scapy/cbor/__init__.py @@ -60,8 +60,8 @@ CBORF_NULL, CBORF_UNDEFINED, CBORF_FLOAT, - CBORF_SEQUENCE, - CBORF_SEQUENCE_OF, + CBORF_ITEMS, + CBORF_REMAINDER_OF, CBORF_ARRAY, CBORF_ARRAY_OF, CBORF_ARRAY_INDEFINITE, @@ -128,8 +128,8 @@ "CBORF_UNDEFINED", "CBORF_FLOAT", # Structured fields - "CBORF_SEQUENCE", - "CBORF_SEQUENCE_OF", + "CBORF_ITEMS", + "CBORF_REMAINDER_OF", "CBORF_ARRAY", "CBORF_ARRAY_OF", "CBORF_ARRAY_INDEFINITE", diff --git a/scapy/cbor/cbor.py b/scapy/cbor/cbor.py index 05054356bbd..65045c83564 100644 --- a/scapy/cbor/cbor.py +++ b/scapy/cbor/cbor.py @@ -343,24 +343,6 @@ class CBOR_KeyKind(metaclass=Enum_metaclass): FINITE = 14 -class CBOR_FingerprintKind(CBOR_KeyKind): - """Discriminator for ``CBORF_ANY`` raw-cache fingerprints. - - Extends :class:`CBOR_KeyKind` with rebuild-sensitive tags that map-key - norms do not need (sentinels, signed zero/inf, ``_encoded`` floats, - Python containers). - """ - name = "CBOR_FINGERPRINT_KIND" - SENTINEL = 15 - FLOAT = 16 - INF = 17 - ZERO = 18 - MAPDATA = 19 - LIST = 20 - DICT = 21 - PY = 22 - - # Recursive hashable RFC 8949 map-key norm. # First element is a ``CBOR_KeyKind`` ``EnumElement`` (typed ``Any`` because # ``Enum_metaclass`` members are ``int`` in the class body). @@ -379,10 +361,6 @@ class CBOR_FingerprintKind(CBOR_KeyKind): Tuple[Any, str, str], ] -# Recursive rebuild-relevant fingerprint for ``CBORF_ANY`` raw-cache state. -# Kind discriminator is ``CBOR_FingerprintKind`` (same EnumElement typing note). -_CBORFingerprint = Tuple[Any, ...] - CBOR_UINT64_MAX = (1 << 64) - 1 diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index fcee3ad09ac..a29ac91aef0 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -22,7 +22,6 @@ CBOR_AdditionalInfo, CBOR_Decoding_Error, CBOR_Encoding_Error, - CBOR_FingerprintKind, CBOR_FloatAI, CBOR_MajorTypes, CBOR_Object, @@ -42,7 +41,6 @@ CBOR_FLOAT, CBOR_MAP, CBOR_SIMPLE_VALUE, - _CBORFingerprint, ) from scapy.cbor.cborcodec import ( CBOR_BREAK_BYTE, @@ -63,6 +61,7 @@ CBORcodec_SIMPLE_AND_FLOAT, ) from scapy.packet import Packet +from scapy.utils import Enum_metaclass from scapy.volatile import ( RandChoice, RandFloat, @@ -192,14 +191,10 @@ def min_items(self, pkt): # type: (CBOR_Packet) -> int return 1 - def max_items(self, pkt): - # type: (CBOR_Packet) -> int - return 1 - def structural_max_items(self, pkt): # type: (CBOR_Packet) -> int """Upper bound independent of not-yet-dissected discriminators.""" - return self.max_items(pkt) + return 1 ########################## @@ -420,6 +415,29 @@ def copy(self): return copy.copy(self) +class _CBORFingerprintKind(metaclass=Enum_metaclass): + """Private discriminator for ``CBORF_ANY`` raw-cache fingerprints.""" + name = "CBOR_FINGERPRINT_KIND" + UNDEF = 3 + ARRAY = 7 + MAP = 8 + TAG = 9 + OBJ = 11 + NAN = 13 + FINITE = 14 + SENTINEL = 15 + FLOAT = 16 + INF = 17 + ZERO = 18 + MAPDATA = 19 + LIST = 20 + DICT = 21 + PY = 22 + + +_CBORFingerprint = Tuple[Any, ...] + + class CBORF_ANY(CBORF_field[Any]): """Represent any well-formed CBOR value as a lossless ``CBOR_Object``.""" ismutable = True @@ -501,7 +519,7 @@ def _cache_fingerprint(obj): """Recursive rebuild-relevant fingerprint for ``CBORF_ANY`` values.""" from scapy.cbor.cbor import CBORMapData fingerprint = CBORF_ANY._cache_fingerprint - Kind = CBOR_FingerprintKind + Kind = _CBORFingerprintKind if obj is CBOR_ABSENT or obj is CBOR_NO_ITEM: return (Kind.SENTINEL, obj) if isinstance(obj, CBOR_UNDEFINED): @@ -545,7 +563,7 @@ def _cache_fingerprint(obj): tag_num, inner = obj.val return (Kind.TAG, int(tag_num), fingerprint(inner)) if isinstance(obj, CBOR_Object): - return (Kind.OBJ, type(obj).__name__, obj.val) + return (Kind.OBJ, type(obj).__name__, fingerprint(obj.val)) if isinstance(obj, list): return (Kind.LIST, tuple(fingerprint(item) for item in obj)) if isinstance(obj, dict): @@ -1154,20 +1172,30 @@ def _build_children(self, pkt): def _dissect_field(self, pkt, field, remaining, max_items=None): # type: (CBOR_Packet, Any, bytes, Optional[int]) -> _CBORParseResult - if isinstance(field, CBORF_SEQUENCE_OF): + if isinstance(field, CBORF_REMAINDER_OF): return field._dissect_counted( pkt, remaining, max_items=max_items ) return field._dissect_counted(pkt, remaining) - def _reject_nonterminal_sequence_of(self): - # type: () -> None - for field in self.seq[:-1]: - if isinstance(field, CBORF_SEQUENCE_OF): - raise ValueError( - "CBORF_SEQUENCE_OF must be the last field " - "in the sequence" - ) + def _reject_nonterminal_remainder_of(self, allow_terminal=True): + # type: (bool) -> None + """Reject ``CBORF_REMAINDER_OF`` that is not a direct final child of *self*. + + Nested compounds never expose a ``CBORF_REMAINDER_OF`` as a direct + child of the outer framing context, so they always recurse with + ``allow_terminal=False``. + """ + for i, field in enumerate(self.seq): + is_last = i == len(self.seq) - 1 + if isinstance(field, CBORF_REMAINDER_OF): + if not (allow_terminal and is_last): + raise ValueError( + "CBORF_REMAINDER_OF must be the last field " + "in the sequence" + ) + elif isinstance(field, _CBORF_compound): + field._reject_nonterminal_remainder_of(allow_terminal=False) def _dissect_children_budgeted(self, pkt, s, count): # type: (CBOR_Packet, bytes, int) -> bytes @@ -1210,7 +1238,7 @@ def _dissect_children_streamed(self, pkt, s): return remaining, total_items -class CBORF_SEQUENCE(_CBORF_compound): +class CBORF_ITEMS(_CBORF_compound): """ Unframed fixed sequence of named, typed fields (no CBOR array head). @@ -1226,7 +1254,7 @@ class CBORF_SEQUENCE(_CBORF_compound): Example:: class MyCBOR(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( + CBOR_root = CBORF_ITEMS( CBORF_INTEGER("version", 1), CBORF_TEXT_STRING("name", ""), ) @@ -1234,8 +1262,8 @@ class MyCBOR(CBOR_Packet): def __init__(self, *seq, **kwargs): # type: (*Any, **Any) -> None - super(CBORF_SEQUENCE, self).__init__(*seq, **kwargs) - self._reject_nonterminal_sequence_of() + super(CBORF_ITEMS, self).__init__(*seq, **kwargs) + self._reject_nonterminal_remainder_of() def _build_counted(self, pkt): # type: (CBOR_Packet) -> _CBORBuildResult @@ -1253,10 +1281,6 @@ 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) - def structural_max_items(self, pkt): # type: (CBOR_Packet) -> int return sum(f.structural_max_items(pkt) for f in self.seq) @@ -1268,7 +1292,7 @@ class CBORF_ARRAY(_CBORF_compound): 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 + Prefer this over :class:`CBORF_ITEMS` when the wire form is a single array item. Example:: @@ -1287,7 +1311,7 @@ class MyCBOR(CBOR_Packet): def __init__(self, *seq, **kwargs): # type: (*Any, **Any) -> None super(CBORF_ARRAY, self).__init__(*seq, **kwargs) - self._reject_nonterminal_sequence_of() + self._reject_nonterminal_remainder_of() def _build_counted(self, pkt): # type: (CBOR_Packet) -> _CBORBuildResult @@ -1424,11 +1448,12 @@ def cache_fingerprint(self, x): @staticmethod def _require_packet_cls(pkt_cls): # type: (Any) -> Type[CBOR_Packet] - """Validate a Packet subclass with CBOR_root for collection elements.""" + """Validate a CBOR_Packet subclass with a non-None CBOR_root.""" + from scapy.cborpacket import CBOR_Packet if ( isinstance(pkt_cls, type) - and issubclass(pkt_cls, Packet) - and hasattr(pkt_cls, "CBOR_root") + and issubclass(pkt_cls, CBOR_Packet) + and getattr(pkt_cls, "CBOR_root", None) is not None ): return cast("Type[CBOR_Packet]", pkt_cls) raise ValueError( @@ -1523,24 +1548,24 @@ def __repr__(self): return "<%s %s>" % (self.__class__.__name__, self.name) -class CBORF_SEQUENCE_OF(_CBORF_HOMOGENEOUS): +class CBORF_REMAINDER_OF(_CBORF_HOMOGENEOUS): """ Unframed sequence of homogeneous elements (no CBOR array head). Preferred constructors (ASN1F_SEQUENCE_OF / PacketListField style):: - CBORF_SEQUENCE_OF("items", [], pkt_cls=MyPacket) - CBORF_SEQUENCE_OF("items", [], pkt_cls=CBORF_UNSIGNED_INTEGER) - CBORF_SEQUENCE_OF("items", [], next_cls_cb=choose_next) + CBORF_REMAINDER_OF("items", [], pkt_cls=MyPacket) + CBORF_REMAINDER_OF("items", [], pkt_cls=CBORF_UNSIGNED_INTEGER) + CBORF_REMAINDER_OF("items", [], next_cls_cb=choose_next) ``pkt_cls`` may be a :class:`CBOR_Packet` subclass or a :class:`CBORF_field` class/instance. Do not use a ``cls=`` keyword: :class:`~typing.Generic` reserves that name on Python 3.7. Pass only one of ``pkt_cls`` / ``next_cls_cb``. - ``SEQUENCE_OF`` represents an unframed sequence of zero or more CBOR - items. Because it consumes the remaining item budget/input, it must be - a direct final child of a positional ``CBORF_SEQUENCE`` or + ``CBORF_REMAINDER_OF`` represents an unframed greedy tail of zero or more + CBOR items. Because it consumes the remaining item budget/input, it must + be a direct final child of a positional ``CBORF_ITEMS`` or ``CBORF_ARRAY``. It must not be wrapped in ``CBORF_optional``, ``CBORF_CONDITIONAL``, or @@ -1560,7 +1585,7 @@ def __init__(self, max_count=None, # type: Optional[int] ): # type: (...) -> None - super(CBORF_SEQUENCE_OF, self).__init__( + super(CBORF_REMAINDER_OF, self).__init__( name, default, pkt_cls=pkt_cls, @@ -1618,7 +1643,7 @@ def min_items(self, pkt): # type: (CBOR_Packet) -> int return 0 - def max_items(self, pkt): + def structural_max_items(self, pkt): # type: (CBOR_Packet) -> int return self._list_limit() @@ -1994,9 +2019,9 @@ def __init__(self, if tag_num < 0 or tag_num > CBOR_UINT64_MAX: raise CBOR_Encoding_Error( "Semantic tag number out of uint64 range") - if isinstance(inner_field, CBORF_SEQUENCE_OF): + if isinstance(inner_field, CBORF_REMAINDER_OF): raise ValueError( - "CBORF_SEQUENCE_OF cannot be wrapped; " + "CBORF_REMAINDER_OF cannot be wrapped; " "place it directly as the final positional field" ) self.tag_num = tag_num @@ -2110,9 +2135,9 @@ def __init__(self, field): "CBORF_optional requires CBORF_field or CBORF_SEMANTIC_TAG; " "got %r" % (type(field).__name__,) ) - if isinstance(field, CBORF_SEQUENCE_OF): + if isinstance(field, CBORF_REMAINDER_OF): raise ValueError( - "CBORF_SEQUENCE_OF cannot be wrapped; " + "CBORF_REMAINDER_OF cannot be wrapped; " "place it directly as the final positional field" ) self._field = field @@ -2138,10 +2163,6 @@ 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) - def structural_max_items(self, pkt): # type: (CBOR_Packet) -> int return self._field.structural_max_items(pkt) @@ -2158,9 +2179,9 @@ def __init__(self, cond, # type: Callable[[Packet], bool] ): # type: (...) -> None - if isinstance(fld, CBORF_SEQUENCE_OF): + if isinstance(fld, CBORF_REMAINDER_OF): raise ValueError( - "CBORF_SEQUENCE_OF cannot be wrapped; " + "CBORF_REMAINDER_OF cannot be wrapped; " "place it directly as the final positional field" ) fields.ConditionalField.__init__(self, fld, cond) @@ -2191,12 +2212,6 @@ def min_items(self, 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 - def structural_max_items(self, pkt): # type: (CBOR_Packet) -> int return self.fld.structural_max_items(pkt) @@ -2220,7 +2235,7 @@ def __init__(self, pkt_cls, # type: Type[CBOR_Packet] ): # type: (...) -> None - self.cls = pkt_cls + self.cls = _CBORF_HOMOGENEOUS._require_packet_cls(pkt_cls) super(CBORF_PACKET, self).__init__(name, default) def m2i(self, pkt, s): diff --git a/test/fields.uts b/test/fields.uts index e2d1132d414..f0092e3d62b 100644 --- a/test/fields.uts +++ b/test/fields.uts @@ -2357,3 +2357,33 @@ p assert p.indent == 0xf assert p.pcount == 4 assert [p.x for p in p.plist] == [0x41, 0x42, 0x43, 0x44] + +############ +############ ++ EnumElement and mutable Field default isolation + += EnumElement copy and deepcopy return the same singleton +~ core field +from copy import copy, deepcopy +from scapy.utils import Enum_metaclass + +class _EnumCopyProbe(metaclass=Enum_metaclass): + name = "ENUM_COPY_PROBE" + A = 1 + B = 2 + +assert copy(_EnumCopyProbe.A) is _EnumCopyProbe.A +assert deepcopy(_EnumCopyProbe.B) is _EnumCopyProbe.B + += Mutable FieldListField defaults are isolated via Field.do_copy +~ core field +class _MutDefaultPkt(Packet): + fields_desc = [ + FieldListField("lst", [1, 2], ByteField("elt", 0)), + ] + +a = _MutDefaultPkt() +b = _MutDefaultPkt() +a.lst.append(3) +assert a.lst == [1, 2, 3] +assert b.lst == [1, 2] diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index 2ffa3a42827..227f916c5bd 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -301,25 +301,25 @@ try: except: True -+ CBORF_SEQUENCE_OF packet item cardinality ++ CBORF_REMAINDER_OF packet item cardinality -= CBORF_SEQUENCE_OF rejects a packet element that consumes two top-level items += CBORF_REMAINDER_OF rejects a packet element that consumes two top-level items from scapy.cbor.cbor import CBOR_Decoding_Error from scapy.cbor.cborfields import ( - CBORF_SEQUENCE, - CBORF_SEQUENCE_OF, + CBORF_ITEMS, + CBORF_REMAINDER_OF, CBORF_UNSIGNED_INTEGER, ) from scapy.cborpacket import CBOR_Packet class TwoItemSequenceDecodeElement(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( + CBOR_root = CBORF_ITEMS( CBORF_UNSIGNED_INTEGER("first", 0), CBORF_UNSIGNED_INTEGER("second", 0), ) class PacketSequenceDecode(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE_OF( + CBOR_root = CBORF_REMAINDER_OF( "elements", [], TwoItemSequenceDecodeElement, @@ -327,7 +327,7 @@ class PacketSequenceDecode(CBOR_Packet): try: PacketSequenceDecode(b"\x01\x02") - assert False, "SEQUENCE_OF accepted two CBOR items as one packet element" + assert False, "REMAINDER_OF accepted two CBOR items as one packet element" except CBOR_Decoding_Error: pass @@ -359,7 +359,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 from scapy.cbor.cborfields import ( - CBORF_SEQUENCE, + CBORF_ITEMS, CBORF_SEMANTIC_TAG, CBORF_UNSIGNED_INTEGER, CBORF_optional, @@ -367,7 +367,7 @@ from scapy.cbor.cborfields import ( from scapy.cborpacket import CBOR_Packet class OptionalTaggedUnsigned(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( + CBOR_root = CBORF_ITEMS( CBORF_optional( CBORF_SEMANTIC_TAG(1, CBORF_UNSIGNED_INTEGER("tagged_value", None)) ) @@ -386,7 +386,7 @@ except CBOR_Decoding_Error: = 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_ITEMS, CBORF_SEMANTIC_TAG, CBORF_UNSIGNED_INTEGER, CBORF_optional, @@ -394,7 +394,7 @@ from scapy.cbor.cborfields import ( from scapy.cborpacket import CBOR_Packet class OptionalTruncatedTaggedUnsigned(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( + CBOR_root = CBORF_ITEMS( CBORF_optional( CBORF_SEMANTIC_TAG(1, CBORF_UNSIGNED_INTEGER("tagged_value", None)) ) @@ -526,10 +526,10 @@ 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 += Packet-valued CBORF_REMAINDER_OF runs child hooks and retains each item span from scapy.cbor.cborfields import ( CBORF_ARRAY, - CBORF_SEQUENCE_OF, + CBORF_REMAINDER_OF, CBORF_UNSIGNED_INTEGER, ) from scapy.cborpacket import CBOR_Packet @@ -548,7 +548,7 @@ class LifecycleSequenceChild(CBOR_Packet): return data class LifecycleSequenceParent(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE_OF("items", [], LifecycleSequenceChild) + CBOR_root = CBORF_REMAINDER_OF("items", [], LifecycleSequenceChild) LifecycleSequenceChild.events[:] = [] pkt = LifecycleSequenceParent(b"\x81\x01") @@ -711,13 +711,13 @@ pkt = AnyArrayFloatPkt(bytes.fromhex("81f90000")) pkt.values[0].val = -0.0 assert bytes(pkt) == bytes.fromhex("81f98000") -= Packet-valued ARRAY_OF and SEQUENCE_OF fingerprint copy and wire cache += Packet-valued ARRAY_OF and REMAINDER_OF fingerprint copy and wire cache from scapy.cbor.cborfields import ( CBORF_ANY, CBORF_ARRAY_OF, CBORF_PACKET, - CBORF_SEQUENCE, - CBORF_SEQUENCE_OF, + CBORF_ITEMS, + CBORF_REMAINDER_OF, ) from scapy.cbor.cbor import CBOR_FLOAT, CBOR_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet @@ -736,13 +736,13 @@ class ArrayOfChildFld(CBOR_Packet): ) class SeqOfChildCls(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( - CBORF_SEQUENCE_OF("items", [], NestedAnyChild), + CBOR_root = CBORF_ITEMS( + CBORF_REMAINDER_OF("items", [], NestedAnyChild), ) class SeqOfChildFld(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( - CBORF_SEQUENCE_OF( + CBOR_root = CBORF_ITEMS( + CBORF_REMAINDER_OF( "items", [], CBORF_PACKET("item", None, NestedAnyChild) ), ) @@ -977,73 +977,129 @@ assert bytes(pkt) == b"\xa1\x61n\x01" + array item reservation -= Nonterminal SEQUENCE_OF is rejected at schema construction += Nonterminal REMAINDER_OF is rejected at schema construction from scapy.cbor.cborfields import ( CBORF_ARRAY, - CBORF_SEQUENCE_OF, + CBORF_REMAINDER_OF, CBORF_UNSIGNED_INTEGER, ) try: CBORF_ARRAY( - CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER), + CBORF_REMAINDER_OF("vals", [], CBORF_UNSIGNED_INTEGER), CBORF_UNSIGNED_INTEGER("tail", 0), ) - assert False, "nonterminal SEQUENCE_OF was accepted" + assert False, "nonterminal REMAINDER_OF was accepted" except ValueError: pass -= Optional cannot wrap SEQUENCE_OF += Nested REMAINDER_OF inside ITEMS is rejected by outer ARRAY from scapy.cbor.cborfields import ( - CBORF_SEQUENCE_OF, + CBORF_ARRAY, + CBORF_ITEMS, + CBORF_REMAINDER_OF, + CBORF_TEXT_STRING, + CBORF_UNSIGNED_INTEGER, +) + +try: + CBORF_ARRAY( + CBORF_ITEMS( + CBORF_UNSIGNED_INTEGER("head", 0), + CBORF_REMAINDER_OF( + "items", [], + pkt_cls=CBORF_UNSIGNED_INTEGER, + ), + ), + CBORF_TEXT_STRING("tail", ""), + ) + assert False, "nested non-direct REMAINDER_OF was accepted" +except ValueError: + pass + += Empty CBOR_Packet without CBOR_root is rejected by ARRAY_OF and PACKET +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_PACKET +from scapy.cborpacket import CBOR_Packet + +class EmptyCBOR(CBOR_Packet): + pass + +try: + CBORF_ARRAY_OF("items", [], pkt_cls=EmptyCBOR) + assert False, "EmptyCBOR accepted by ARRAY_OF" +except ValueError: + pass + +try: + CBORF_PACKET("child", None, EmptyCBOR) + assert False, "EmptyCBOR accepted by PACKET" +except ValueError: + pass + += Generic CBOR_Object fingerprint recurses into mutable .val +from scapy.cbor.cbor import CBOR_Object, CBOR_UNSIGNED_INTEGER +from scapy.cbor.cborfields import CBORF_ANY + +class MutableListObj(CBOR_Object): + tag = None # type: ignore + +obj = MutableListObj([CBOR_UNSIGNED_INTEGER(1)]) +snap = CBORF_ANY._cache_fingerprint(obj) +obj.val.append(CBOR_UNSIGNED_INTEGER(2)) +# Snapshot must not alias .val; otherwise in-place mutation keeps equality. +assert CBORF_ANY._cache_fingerprint(obj) != snap + += Optional cannot wrap REMAINDER_OF +from scapy.cbor.cborfields import ( + CBORF_REMAINDER_OF, CBORF_UNSIGNED_INTEGER, CBORF_optional, ) try: CBORF_optional( - CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER) + CBORF_REMAINDER_OF("vals", [], CBORF_UNSIGNED_INTEGER) ) - assert False, "optional SEQUENCE_OF was accepted" + assert False, "optional REMAINDER_OF was accepted" except ValueError: pass -= Conditional cannot wrap SEQUENCE_OF += Conditional cannot wrap REMAINDER_OF from scapy.cbor.cborfields import ( CBORF_CONDITIONAL, - CBORF_SEQUENCE_OF, + CBORF_REMAINDER_OF, CBORF_UNSIGNED_INTEGER, ) try: CBORF_CONDITIONAL( - CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER), + CBORF_REMAINDER_OF("vals", [], CBORF_UNSIGNED_INTEGER), lambda pkt: True, ) - assert False, "conditional SEQUENCE_OF was accepted" + assert False, "conditional REMAINDER_OF was accepted" except ValueError: pass -= CBORF_SEMANTIC_TAG cannot wrap CBORF_SEQUENCE_OF += CBORF_SEMANTIC_TAG cannot wrap CBORF_REMAINDER_OF from scapy.cbor.cborfields import ( CBORF_SEMANTIC_TAG, - CBORF_SEQUENCE_OF, + CBORF_REMAINDER_OF, CBORF_UNSIGNED_INTEGER, ) try: CBORF_SEMANTIC_TAG( 1, - CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER), + CBORF_REMAINDER_OF("vals", [], CBORF_UNSIGNED_INTEGER), ) - assert False, "semantic-tagged SEQUENCE_OF should be rejected" + assert False, "semantic-tagged REMAINDER_OF should be rejected" except ValueError: pass -= Optional semantic tag cannot hide CBORF_SEQUENCE_OF += Optional semantic tag cannot hide CBORF_REMAINDER_OF from scapy.cbor.cborfields import ( CBORF_SEMANTIC_TAG, - CBORF_SEQUENCE_OF, + CBORF_REMAINDER_OF, CBORF_UNSIGNED_INTEGER, CBORF_optional, ) @@ -1052,10 +1108,10 @@ try: CBORF_optional( CBORF_SEMANTIC_TAG( 1, - CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER), + CBORF_REMAINDER_OF("vals", [], CBORF_UNSIGNED_INTEGER), ) ) - assert False, "wrapped semantic-tagged SEQUENCE_OF should be rejected" + assert False, "wrapped semantic-tagged REMAINDER_OF should be rejected" except ValueError: pass @@ -1100,10 +1156,10 @@ pkt = TaggedStruct(b"\xc1\x82\x01\x02") assert pkt.a == 1 and pkt.b == 2 assert bytes(pkt) == b"\xc1\x82\x01\x02" -= Direct terminal SEQUENCE_OF respects definite array item budget += Direct terminal REMAINDER_OF respects definite array item budget from scapy.cbor.cborfields import ( CBORF_ARRAY, - CBORF_SEQUENCE_OF, + CBORF_REMAINDER_OF, CBORF_UNSIGNED_INTEGER, ) from scapy.cborpacket import CBOR_Packet @@ -1112,7 +1168,7 @@ from scapy.packet import Raw class DirectTerminal(CBOR_Packet): CBOR_root = CBORF_ARRAY( CBORF_UNSIGNED_INTEGER("n", 0), - CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER), + CBORF_REMAINDER_OF("vals", [], CBORF_UNSIGNED_INTEGER), ) pkt = DirectTerminal() @@ -1128,10 +1184,10 @@ assert pkt.n == 1 assert pkt.vals == [10] assert isinstance(pkt.payload, Raw) and pkt.payload.load == b"\x0b" -= Direct terminal SEQUENCE_OF in indefinite ARRAY stops at break += Direct terminal REMAINDER_OF in indefinite ARRAY stops at break from scapy.cbor.cborfields import ( CBORF_ARRAY_INDEFINITE, - CBORF_SEQUENCE_OF, + CBORF_REMAINDER_OF, CBORF_UNSIGNED_INTEGER, ) from scapy.cborpacket import CBOR_Packet @@ -1139,17 +1195,17 @@ from scapy.cborpacket import CBOR_Packet class DirectTerminalIndef(CBOR_Packet): CBOR_root = CBORF_ARRAY_INDEFINITE( CBORF_UNSIGNED_INTEGER("n", 0), - CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER), + CBORF_REMAINDER_OF("vals", [], CBORF_UNSIGNED_INTEGER), ) pkt = DirectTerminalIndef(b"\x9f\x01\x0a\x0b\xff") assert pkt.n == 1 assert pkt.vals == [10, 11] -= Empty terminal SEQUENCE_OF leaves an empty list += Empty terminal REMAINDER_OF leaves an empty list from scapy.cbor.cborfields import ( CBORF_ARRAY, - CBORF_SEQUENCE_OF, + CBORF_REMAINDER_OF, CBORF_UNSIGNED_INTEGER, ) from scapy.cborpacket import CBOR_Packet @@ -1157,18 +1213,18 @@ from scapy.cborpacket import CBOR_Packet class EmptyTerminal(CBOR_Packet): CBOR_root = CBORF_ARRAY( CBORF_UNSIGNED_INTEGER("n", 0), - CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER), + CBORF_REMAINDER_OF("vals", [], CBORF_UNSIGNED_INTEGER), ) pkt = EmptyTerminal(b"\x81\x05") assert pkt.n == 5 assert pkt.vals == [] -= Optional before trailing SEQUENCE_OF does not invent a reserved count += Optional before trailing REMAINDER_OF does not invent a reserved count from scapy.cbor.cborfields import ( CBORF_ARRAY, CBORF_BOOLEAN, - CBORF_SEQUENCE_OF, + CBORF_REMAINDER_OF, CBORF_UNSIGNED_INTEGER, CBORF_optional, CBOR_ABSENT, @@ -1178,10 +1234,10 @@ from scapy.cborpacket import CBOR_Packet class OptThenSeqOf(CBOR_Packet): CBOR_root = CBORF_ARRAY( CBORF_optional(CBORF_UNSIGNED_INTEGER("opt", None)), - CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER), + CBORF_REMAINDER_OF("vals", [], CBORF_UNSIGNED_INTEGER), ) -# Terminal SEQUENCE_OF reserves nothing; same-type optional may take the item. +# Terminal REMAINDER_OF reserves nothing; same-type optional may take the item. pkt = OptThenSeqOf(b"\x81\x07") assert pkt.opt == 7 assert pkt.vals == [] @@ -1189,10 +1245,10 @@ assert pkt.vals == [] class BoolThenSeqOf(CBOR_Packet): CBOR_root = CBORF_ARRAY( CBORF_optional(CBORF_BOOLEAN("opt", None)), - CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER), + CBORF_REMAINDER_OF("vals", [], CBORF_UNSIGNED_INTEGER), ) -# Type mismatch leaves the item for the trailing SEQUENCE_OF. +# Type mismatch leaves the item for the trailing REMAINDER_OF. pkt = BoolThenSeqOf(b"\x81\x07") assert pkt.opt is CBOR_ABSENT assert pkt.vals == [7] @@ -1291,7 +1347,7 @@ from scapy.cbor.cborfields import ( CBORF_ARRAY_INDEFINITE, CBORF_CONDITIONAL, CBORF_INTEGER, - CBORF_SEQUENCE, + CBORF_ITEMS, ) from scapy.cborpacket import CBOR_Packet @@ -1302,7 +1358,7 @@ class CondOffArray(CBOR_Packet): ) class CondOffSeq(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( + CBOR_root = CBORF_ITEMS( CBORF_INTEGER("flag", 0), CBORF_CONDITIONAL(CBORF_INTEGER("last", 8), lambda p: p.flag == 1), ) @@ -1340,19 +1396,19 @@ try: except CBOR_Decoding_Error: pass -= Indefinite array rejects nonterminal SEQUENCE_OF += Indefinite array rejects nonterminal REMAINDER_OF from scapy.cbor.cborfields import ( CBORF_ARRAY_INDEFINITE, - CBORF_SEQUENCE_OF, + CBORF_REMAINDER_OF, CBORF_UNSIGNED_INTEGER, ) try: CBORF_ARRAY_INDEFINITE( - CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER), + CBORF_REMAINDER_OF("vals", [], CBORF_UNSIGNED_INTEGER), CBORF_UNSIGNED_INTEGER("tail", 0), ) - assert False, "nonterminal SEQUENCE_OF was accepted" + assert False, "nonterminal REMAINDER_OF was accepted" except ValueError: pass @@ -1386,8 +1442,8 @@ from scapy.cbor.cborfields import ( CBORF_NULL, CBORF_PACKET, CBORF_SEMANTIC_TAG, - CBORF_SEQUENCE, - CBORF_SEQUENCE_OF, + CBORF_ITEMS, + CBORF_REMAINDER_OF, CBORF_TEXT_STRING, CBORF_UNDEFINED, CBORF_UNSIGNED_INTEGER, @@ -1476,7 +1532,7 @@ assert bytes(pkt) == b"\x82\xa2\x01\x61i\xf5\x61b\x01" = Optional Boolean does not consume a following floating-point value class RROptionalBooleanThenFloat(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( + CBOR_root = CBORF_ITEMS( CBORF_optional(CBORF_BOOLEAN("maybe", None)), CBORF_FLOAT("value", None), ) @@ -1486,7 +1542,7 @@ assert pkt.value == 1.5 = Optional Boolean does not consume a following null class RROptionalBooleanThenNull(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( + CBOR_root = CBORF_ITEMS( CBORF_optional(CBORF_BOOLEAN("maybe", None)), CBORF_NULL("value"), ) @@ -1496,7 +1552,7 @@ assert bytes(pkt) == b"\xf6" = Optional null does not consume a following Boolean class RROptionalNullThenBoolean(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( + CBOR_root = CBORF_ITEMS( CBORF_optional(CBORF_NULL("maybe")), CBORF_BOOLEAN("value", None), ) @@ -1506,7 +1562,7 @@ assert pkt.value is True = Optional undefined does not consume a following float class RROptionalUndefinedThenFloat(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( + CBOR_root = CBORF_ITEMS( CBORF_optional(CBORF_UNDEFINED("maybe")), CBORF_FLOAT("value", None), ) @@ -1516,7 +1572,7 @@ assert pkt.value == 1.5 = Optional float does not consume a following Boolean class RROptionalFloatThenBoolean(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( + CBOR_root = CBORF_ITEMS( CBORF_optional(CBORF_FLOAT("maybe", None)), CBORF_BOOLEAN("value", None), ) @@ -1526,13 +1582,13 @@ assert pkt.value is False = Exact major-type-7 matches are still consumed by optional fields class RROptionalBooleanPresent(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( + CBOR_root = CBORF_ITEMS( CBORF_optional(CBORF_BOOLEAN("maybe", None)), CBORF_UNSIGNED_INTEGER("tail", None), ) class RROptionalFloatPresent(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( + CBOR_root = CBORF_ITEMS( CBORF_optional(CBORF_FLOAT("maybe", None)), CBORF_UNSIGNED_INTEGER("tail", None), ) @@ -1552,7 +1608,7 @@ for wire in (b"\xf9\x3e\x00", b"\xfa\x3f\xc0\x00\x00"): = Optional Boolean leaves direct and extended simple values for CBORF_ANY class RROptionalBooleanThenAny(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( + CBOR_root = CBORF_ITEMS( CBORF_optional(CBORF_BOOLEAN("maybe", None)), CBORF_ANY("value", None), ) @@ -1565,14 +1621,14 @@ for wire, expected in ((b"\xf0", 16), (b"\xf8\x20", 32)): = Optional null and undefined do not consume each other's wire values class RROptionalNullThenUndefined(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( + CBOR_root = CBORF_ITEMS( CBORF_optional(CBORF_NULL("maybe")), CBORF_UNDEFINED("value"), CBORF_UNSIGNED_INTEGER("tail", 0), ) class RROptionalUndefinedThenNull(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( + CBOR_root = CBORF_ITEMS( CBORF_optional(CBORF_UNDEFINED("maybe")), CBORF_NULL("value"), CBORF_UNSIGNED_INTEGER("tail", 0), @@ -1704,31 +1760,31 @@ assert bytes(pkt) == b"\x82\xf6\x01" + Finding 4: positional arrays must reserve items for later required fields -= Definite arrays reject nonterminal SEQUENCE_OF += Definite arrays reject nonterminal REMAINDER_OF try: CBORF_ARRAY( - CBORF_SEQUENCE_OF( + CBORF_REMAINDER_OF( "values", [], CBORF_UNSIGNED_INTEGER("item", None), ), CBORF_UNSIGNED_INTEGER("tail", None), ) - assert False, "nonterminal SEQUENCE_OF accepted" + assert False, "nonterminal REMAINDER_OF accepted" except ValueError: pass -= Indefinite arrays reject nonterminal SEQUENCE_OF += Indefinite arrays reject nonterminal REMAINDER_OF try: CBORF_ARRAY_INDEFINITE( - CBORF_SEQUENCE_OF( + CBORF_REMAINDER_OF( "values", [], CBORF_UNSIGNED_INTEGER("item", None), ), CBORF_UNSIGNED_INTEGER("tail", None), ) - assert False, "nonterminal SEQUENCE_OF accepted" + assert False, "nonterminal REMAINDER_OF accepted" except ValueError: pass @@ -1757,11 +1813,11 @@ class RROptionalThenRequiredPacket(CBOR_Packet): pkt = RROptionalThenRequiredPacket(b"\x81\x07") assert pkt.required_child.value == 7 -= SEQUENCE_OF before a required conditional field is rejected += REMAINDER_OF before a required conditional field is rejected try: CBORF_ARRAY( CBORF_UNSIGNED_INTEGER("flag", 0), - CBORF_SEQUENCE_OF( + CBORF_REMAINDER_OF( "values", [], CBORF_UNSIGNED_INTEGER("item", None), @@ -1771,7 +1827,7 @@ try: lambda pkt: pkt.getfieldval("flag") == 1, ), ) - assert False, "nonterminal SEQUENCE_OF was accepted" + assert False, "nonterminal REMAINDER_OF was accepted" except ValueError: pass @@ -1882,9 +1938,9 @@ RRCountingArray.calls = 0 bytes(RRCountedArrayParent(children=[RRCountedChild(value=1)])) assert RRCountingArray.calls == 1 -= Packet-valued CBORF_SEQUENCE_OF builds each child root exactly once += Packet-valued CBORF_REMAINDER_OF builds each child root exactly once class RRCountedSequenceParent(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE_OF("children", [], RRCountedChild) + CBOR_root = CBORF_REMAINDER_OF("children", [], RRCountedChild) RRCountingArray.calls = 0 bytes(RRCountedSequenceParent(children=[RRCountedChild(value=1)])) @@ -2003,12 +2059,12 @@ except CBOR_Codec_Decoding_Error: = Two adjacent unbounded positional sequences are rejected as ambiguous try: CBORF_ARRAY( - CBORF_SEQUENCE_OF( + CBORF_REMAINDER_OF( "left", [], CBORF_UNSIGNED_INTEGER("left_item", None), ), - CBORF_SEQUENCE_OF( + CBORF_REMAINDER_OF( "right", [], CBORF_UNSIGNED_INTEGER("right_item", None), @@ -2226,20 +2282,20 @@ pkt = OptionalPacketThenAny(b"\x81\xf5") assert pkt.getfieldval("child") is CBOR_ABSENT assert pkt.required.val is True -= Nonterminal SEQUENCE_OF is rejected in unframed sequences += Nonterminal REMAINDER_OF is rejected in unframed sequences from scapy.cbor.cborfields import ( - CBORF_SEQUENCE, - CBORF_SEQUENCE_OF, + CBORF_ITEMS, + CBORF_REMAINDER_OF, CBORF_TEXT_STRING, CBORF_UNSIGNED_INTEGER, ) try: - CBORF_SEQUENCE( - CBORF_SEQUENCE_OF("items", [], CBORF_UNSIGNED_INTEGER), + CBORF_ITEMS( + CBORF_REMAINDER_OF("items", [], CBORF_UNSIGNED_INTEGER), CBORF_TEXT_STRING("tail", ""), ) - assert False, "nonterminal SEQUENCE_OF accepted" + assert False, "nonterminal REMAINDER_OF accepted" except ValueError: pass @@ -2247,16 +2303,16 @@ except ValueError: from scapy.cbor.cborfields import ( CBORF_ARRAY, CBORF_BOOLEAN, - CBORF_SEQUENCE_OF, + CBORF_REMAINDER_OF, CBORF_UNSIGNED_INTEGER, CBORF_optional, ) try: CBORF_ARRAY( - CBORF_SEQUENCE_OF("left", [], CBORF_UNSIGNED_INTEGER), + CBORF_REMAINDER_OF("left", [], CBORF_UNSIGNED_INTEGER), CBORF_optional(CBORF_BOOLEAN("middle", None)), - CBORF_SEQUENCE_OF("right", [], CBORF_UNSIGNED_INTEGER), + CBORF_REMAINDER_OF("right", [], CBORF_UNSIGNED_INTEGER), ) except ValueError: pass @@ -2310,10 +2366,10 @@ assert bytes(pkt.child) == b"\x9f\x01\xff" pkt.sibling = 1 assert bytes(pkt) == b"\x82\x01\x9f\x01\xff" -= SEQUENCE_OF preserves raw representations of untouched packet children += REMAINDER_OF preserves raw representations of untouched packet children from scapy.cbor.cborfields import ( CBORF_ARRAY, - CBORF_SEQUENCE_OF, + CBORF_REMAINDER_OF, CBORF_UNSIGNED_INTEGER, ) from scapy.cborpacket import CBOR_Packet @@ -2326,7 +2382,7 @@ class SequenceArrayChild(CBOR_Packet): class ParentWithChildSequence(CBOR_Packet): CBOR_root = CBORF_ARRAY( CBORF_UNSIGNED_INTEGER("sibling", 0), - CBORF_SEQUENCE_OF("children", [], SequenceArrayChild), + CBORF_REMAINDER_OF("children", [], SequenceArrayChild), ) wire = b"\x83\x00\x9f\x01\xff\x81\x02" @@ -2729,14 +2785,14 @@ assert cbor_find_non_deterministic( = Unframed SEQUENCE cache returns exact bytes without rebuild from scapy.cbor.cborfields import ( CBORF_PACKET, - CBORF_SEQUENCE, + CBORF_ITEMS, CBORF_UNSIGNED_INTEGER, ) from scapy.cbor.cbor import CBOR_Encoding_Error from scapy.cborpacket import CBOR_Packet class SeqChild(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( + CBOR_root = CBORF_ITEMS( CBORF_UNSIGNED_INTEGER("a", 0), CBORF_UNSIGNED_INTEGER("b", 0), ) @@ -2878,7 +2934,7 @@ assert bytes(pkt) == raw from scapy.cbor.cbor import CBOR_Decoding_Error from scapy.cbor.cborfields import ( CBORF_BOOLEAN, - CBORF_SEQUENCE, + CBORF_ITEMS, CBORF_UNSIGNED_INTEGER, CBORF_optional, CBOR_ABSENT, @@ -2886,7 +2942,7 @@ from scapy.cbor.cborfields import ( from scapy.cborpacket import CBOR_Packet class GreedySequence(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( + CBOR_root = CBORF_ITEMS( CBORF_optional(CBORF_UNSIGNED_INTEGER("optional", None)), CBORF_UNSIGNED_INTEGER("required", 0), ) @@ -2902,7 +2958,7 @@ except CBOR_Decoding_Error: pass class TypedSequence(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( + CBOR_root = CBORF_ITEMS( CBORF_optional(CBORF_BOOLEAN("optional", None)), CBORF_UNSIGNED_INTEGER("required", 0), ) @@ -2924,14 +2980,14 @@ assert result.remaining == b"\x18" = Optional-only SEQUENCE marks absent on empty input from scapy.cbor.cborfields import ( CBORF_BOOLEAN, - CBORF_SEQUENCE, + CBORF_ITEMS, CBORF_optional, CBOR_ABSENT, ) from scapy.cborpacket import CBOR_Packet class OptionalOnly(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( + CBOR_root = CBORF_ITEMS( CBORF_optional(CBORF_BOOLEAN("maybe", None)), ) @@ -2954,14 +3010,14 @@ except CBOR_Encoding_Error: = CBORF_PACKET _build_value enforces one-item cardinality like _build_counted from scapy.cbor.cborfields import ( CBORF_PACKET, - CBORF_SEQUENCE, + CBORF_ITEMS, CBORF_UNSIGNED_INTEGER, ) from scapy.cbor.cbor import CBOR_Encoding_Error from scapy.cborpacket import CBOR_Packet class TwoItemChild(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( + CBOR_root = CBORF_ITEMS( CBORF_UNSIGNED_INTEGER("a", 1), CBORF_UNSIGNED_INTEGER("b", 2), ) @@ -3011,12 +3067,15 @@ class ForcedAnyPkt(CBOR_Packet): bytes(ForcedAnyPkt(value=0)) == b"\x81\x18\x63" = CBORF_PACKET rejects non-CBOR Packet/bytes that are not exactly one item -from scapy.cbor.cborfields import CBORF_PACKET +from scapy.cbor.cborfields import CBORF_PACKET, CBORF_UNSIGNED_INTEGER from scapy.cbor.cbor import CBOR_Encoding_Error from scapy.cborpacket import CBOR_Packet from scapy.packet import Raw -fld = CBORF_PACKET("x", None, pkt_cls=CBOR_Packet) +class OneItemPkt(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("n", 0) + +fld = CBORF_PACKET("x", None, pkt_cls=OneItemPkt) # Two valid CBOR integers must not be reported as one item try: @@ -3164,11 +3223,11 @@ for child in parent.children: 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 += CBORF_REMAINDER_OF construction attaches every packet child to parent +from scapy.cbor.cborfields import CBORF_REMAINDER_OF class SequenceParent(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE_OF("children", [], OwnedChild) + CBOR_root = CBORF_REMAINDER_OF("children", [], OwnedChild) children = [OwnedChild(value=1), OwnedChild(value=2)] parent = SequenceParent(children=children) @@ -3179,7 +3238,7 @@ for child in parent.children: assert bytes(parent) == b"\x01\x02" -= CBORF_SEQUENCE_OF dissection attaches every packet child to parent += CBORF_REMAINDER_OF dissection attaches every packet child to parent parent = SequenceParent(b"\x01\x02") assert [child.value for child in parent.children] == [1, 2] for child in parent.children: @@ -3335,8 +3394,8 @@ assert bytes(pkt) == b"\x43abc" + PacketListField-style next_cls_cb semantics -= CBORF_SEQUENCE_OF next_cls_cb selects packet classes dynamically -from scapy.cbor.cborfields import CBORF_SEQUENCE_OF, CBORF_UNSIGNED_INTEGER += CBORF_REMAINDER_OF next_cls_cb selects packet classes dynamically +from scapy.cbor.cborfields import CBORF_REMAINDER_OF, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet class DynamicSequenceChild(CBOR_Packet): @@ -3348,7 +3407,7 @@ def choose_dynamic_child(pkt, lst, cur, remain): return DynamicSequenceChild class DynamicSequenceParent(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE_OF( + CBOR_root = CBORF_REMAINDER_OF( "children", [], next_cls_cb=choose_dynamic_child, @@ -3360,15 +3419,15 @@ 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 pkt_cls -from scapy.cbor.cborfields import CBORF_SEQUENCE_OF, CBORF_UNSIGNED_INTEGER += CBORF_REMAINDER_OF rejects combining next_cls_cb with pkt_cls +from scapy.cbor.cborfields import CBORF_REMAINDER_OF, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet class FixedSequenceChild(CBOR_Packet): CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) try: - CBORF_SEQUENCE_OF( + CBORF_REMAINDER_OF( "children", [], pkt_cls=FixedSequenceChild, @@ -3377,22 +3436,22 @@ try: except ValueError: pass else: - raise AssertionError("conflicting SEQUENCE_OF selectors accepted") + raise AssertionError("conflicting REMAINDER_OF selectors accepted") -= CBORF_SEQUENCE_OF rejects a packet instance as pkt_cls -from scapy.cbor.cborfields import CBORF_SEQUENCE_OF, CBORF_UNSIGNED_INTEGER += CBORF_REMAINDER_OF rejects a packet instance as pkt_cls +from scapy.cbor.cborfields import CBORF_REMAINDER_OF, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet class SeqOfChild(CBOR_Packet): CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) -CBORF_SEQUENCE_OF("ok", [], pkt_cls=SeqOfChild) +CBORF_REMAINDER_OF("ok", [], pkt_cls=SeqOfChild) try: - CBORF_SEQUENCE_OF("bad", [], pkt_cls=SeqOfChild()) + CBORF_REMAINDER_OF("bad", [], pkt_cls=SeqOfChild()) except ValueError: pass else: - raise AssertionError("SEQUENCE_OF accepted a packet instance as pkt_cls") + raise AssertionError("REMAINDER_OF accepted a packet instance as pkt_cls") = CBORF_ARRAY_OF rejects a packet instance as pkt_cls from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_UNSIGNED_INTEGER @@ -3409,10 +3468,10 @@ 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 += CBORF_REMAINDER_OF and ARRAY_OF reject fake CBOR_root classes from scapy.cbor.cborfields import ( CBORF_ARRAY_OF, - CBORF_SEQUENCE_OF, + CBORF_REMAINDER_OF, CBORF_UNSIGNED_INTEGER, ) @@ -3420,11 +3479,11 @@ class FakeRoot(object): CBOR_root = CBORF_UNSIGNED_INTEGER("x", 0) try: - CBORF_SEQUENCE_OF("items", [], pkt_cls=FakeRoot) + CBORF_REMAINDER_OF("items", [], pkt_cls=FakeRoot) except ValueError: pass else: - raise AssertionError("SEQUENCE_OF accepted a non-Packet CBOR_root class") + raise AssertionError("REMAINDER_OF accepted a non-Packet CBOR_root class") try: CBORF_ARRAY_OF("items", [], pkt_cls=FakeRoot) @@ -3433,20 +3492,20 @@ except ValueError: else: raise AssertionError("ARRAY_OF accepted a non-Packet CBOR_root class") -= CBORF_SEQUENCE_OF and ARRAY_OF reject plain Packet without CBOR_root += CBORF_REMAINDER_OF and ARRAY_OF reject plain Packet without CBOR_root from scapy.packet import Packet from scapy.fields import ByteField -from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_SEQUENCE_OF +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_REMAINDER_OF class PlainPacket(Packet): fields_desc = [ByteField("x", 0)] try: - CBORF_SEQUENCE_OF("items", [], pkt_cls=PlainPacket) + CBORF_REMAINDER_OF("items", [], pkt_cls=PlainPacket) except ValueError: pass else: - raise AssertionError("SEQUENCE_OF accepted Packet without CBOR_root") + raise AssertionError("REMAINDER_OF accepted Packet without CBOR_root") try: CBORF_ARRAY_OF("items", [], pkt_cls=PlainPacket) @@ -3458,9 +3517,9 @@ else: + TypeError must not be used for callback signature probing -= CBORF_SEQUENCE_OF does not retry next_cls_cb when the callback itself raises TypeError += CBORF_REMAINDER_OF does not retry next_cls_cb when the callback itself raises TypeError from scapy.cbor.cbor import CBOR_Decoding_Error -from scapy.cbor.cborfields import CBORF_SEQUENCE_OF +from scapy.cbor.cborfields import CBORF_REMAINDER_OF from scapy.cborpacket import CBOR_Packet BrokenNextClsCalls = [] @@ -3469,7 +3528,7 @@ def broken_next_cls(*args): raise TypeError("intentional next_cls_cb failure") class BrokenCallbackParent(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE_OF( + CBOR_root = CBORF_REMAINDER_OF( "children", [], next_cls_cb=broken_next_cls, @@ -3763,20 +3822,20 @@ finally: conf.max_list_count = old -= CBORF_SEQUENCE_OF respects max_count += CBORF_REMAINDER_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.cbor.cborfields import CBORF_REMAINDER_OF, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet class LimitedSeq(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE_OF( + CBOR_root = CBORF_REMAINDER_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" + assert False, "REMAINDER_OF ignored max_count" except CBOR_Decoding_Error: pass @@ -3800,12 +3859,12 @@ assert bytes(pkt) == wire # preferred encoding for 1.5 is half pkt.value = 1.5 assert bytes(pkt) == wire -= Unframed CBORF_SEQUENCE leaves trailing CBOR items -from scapy.cbor.cborfields import CBORF_SEQUENCE, CBORF_UNSIGNED_INTEGER += Unframed CBORF_ITEMS leaves trailing CBOR items +from scapy.cbor.cborfields import CBORF_ITEMS, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet class TwoInts(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( + CBOR_root = CBORF_ITEMS( CBORF_UNSIGNED_INTEGER("a", 0), CBORF_UNSIGNED_INTEGER("b", 0), ) @@ -4081,7 +4140,7 @@ from scapy.cbor.cborcodec import ( ) from scapy.cbor.cborfields import ( CBORF_ARRAY_INDEFINITE, - CBORF_SEQUENCE, + CBORF_ITEMS, CBORF_UNSIGNED_INTEGER, ) from scapy.cborpacket import CBOR_Packet @@ -4097,7 +4156,7 @@ except RecursionError: assert False, "pre-scan leaked RecursionError" class DeepSeq(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE(CBORF_UNSIGNED_INTEGER("n", 0)) + CBOR_root = CBORF_ITEMS(CBORF_UNSIGNED_INTEGER("n", 0)) try: DeepSeq(too_deep) @@ -4724,11 +4783,19 @@ class LifeMap2(CBOR_Packet): unknown_field="unknown_pairs", ) -pkt = LifeMap2(b"\xa2\x61a\x01\x61x\x0a") -fuzzed = fuzz(pkt) +wire = b"\xa2\x61a\x01\x61x\x0a" +fuzzed = fuzz(LifeMap2()) +assert isinstance(fuzzed, LifeMap2) +bytes(fuzzed) +fuzzed.show(dump=True) +fuzzed.show2(dump=True) + +parsed = LifeMap2(wire) +fuzzed = fuzz(parsed) assert isinstance(fuzzed, LifeMap2) -pkt.show(dump=True) -pkt.show2(dump=True) +bytes(fuzzed) +fuzzed.show(dump=True) +fuzzed.show2(dump=True) = copy and deepcopy re-parent nested CBOR packets import copy From 01bbc78aae0b4f37ce473dc42374e66140af7d88 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Fri, 11 Sep 2026 13:16:39 +0200 Subject: [PATCH 63/65] cbor: allow nested framed REMAINDER_OF; bound MAP dissect Recurse remainder rejection only through unframed ITEMS, apply conf.max_list_count to MAP, and tighten Packet default-copy coverage. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborfields.py | 104 ++++++++++++++++++++----------------- test/fields.uts | 18 ++++--- test/scapy/layers/cbor.uts | 101 +++++++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+), 56 deletions(-) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index a29ac91aef0..8f76381cfbf 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -1182,9 +1182,9 @@ def _reject_nonterminal_remainder_of(self, allow_terminal=True): # type: (bool) -> None """Reject ``CBORF_REMAINDER_OF`` that is not a direct final child of *self*. - Nested compounds never expose a ``CBORF_REMAINDER_OF`` as a direct - child of the outer framing context, so they always recurse with - ``allow_terminal=False``. + Nested unframed ``CBORF_ITEMS`` share this framing context, so they + recurse with ``allow_terminal=False``. Framed ``CBORF_ARRAY`` + compounds establish their own item budget and are not walked. """ for i, field in enumerate(self.seq): is_last = i == len(self.seq) - 1 @@ -1194,49 +1194,11 @@ def _reject_nonterminal_remainder_of(self, allow_terminal=True): "CBORF_REMAINDER_OF must be the last field " "in the sequence" ) - elif isinstance(field, _CBORF_compound): + elif isinstance(field, _CBORF_compound) and field.CBOR_tag is None: + # Unframed ITEMS share this framing context; framed ARRAY + # establishes its own count boundary and validates itself. field._reject_nonterminal_remainder_of(allow_terminal=False) - def _dissect_children_budgeted(self, pkt, s, count): - # type: (CBOR_Packet, bytes, int) -> bytes - remaining = s - items_left = count - for index, field in enumerate(self.seq): - reserved = sum( - f.min_items(pkt) for f in self.seq[index + 1:] - ) - available = items_left - reserved - needed = field.min_items(pkt) - if available < 0 or available < needed: - raise CBOR_Decoding_Error("CBOR item count mismatch") - if available == 0: - if isinstance(field, CBORF_optional): - field._field.mark_absent(pkt) - continue - result = self._dissect_field( - pkt, field, remaining, max_items=available - ) - if result.items > items_left: - raise CBOR_Decoding_Error( - "CBOR field consumed more items than remaining" - ) - remaining = result.remaining - items_left -= result.items - if items_left != 0: - raise CBOR_Decoding_Error("CBOR item count mismatch") - return remaining - - def _dissect_children_streamed(self, pkt, s): - # type: (CBOR_Packet, bytes) -> Tuple[bytes, int] - """Consume schema fields greedily left-to-right; leave trailing bytes.""" - remaining = s - total_items = 0 - for field in self.seq: - result = self._dissect_field(pkt, field, remaining) - remaining = result.remaining - total_items += result.items - return remaining, total_items - class CBORF_ITEMS(_CBORF_compound): """ @@ -1272,10 +1234,15 @@ def _build_counted(self, pkt): def _dissect_counted(self, pkt, s): # type: (CBOR_Packet, bytes) -> _CBORParseResult - # Stream schema fields only; leave trailing bytes for the parent - # without requiring them to be well-formed CBOR. - remaining, items = self._dissect_children_streamed(pkt, s) - return _CBORParseResult(remaining=remaining, items=items) + # Stream schema fields greedily left-to-right; leave trailing bytes + # for the parent without requiring them to be well-formed CBOR. + remaining = s + total_items = 0 + for field in self.seq: + result = self._dissect_field(pkt, field, remaining) + remaining = result.remaining + total_items += result.items + return _CBORParseResult(remaining=remaining, items=total_items) def min_items(self, pkt): # type: (CBOR_Packet) -> int @@ -1313,6 +1280,35 @@ def __init__(self, *seq, **kwargs): super(CBORF_ARRAY, self).__init__(*seq, **kwargs) self._reject_nonterminal_remainder_of() + def _dissect_children_budgeted(self, pkt, s, count): + # type: (CBOR_Packet, bytes, int) -> bytes + remaining = s + items_left = count + for index, field in enumerate(self.seq): + reserved = sum( + f.min_items(pkt) for f in self.seq[index + 1:] + ) + available = items_left - reserved + needed = field.min_items(pkt) + if available < 0 or available < needed: + raise CBOR_Decoding_Error("CBOR item count mismatch") + if available == 0: + if isinstance(field, CBORF_optional): + field._field.mark_absent(pkt) + continue + result = self._dissect_field( + pkt, field, remaining, max_items=available + ) + if result.items > items_left: + raise CBOR_Decoding_Error( + "CBOR field consumed more items than remaining" + ) + remaining = result.remaining + items_left -= result.items + if items_left != 0: + raise CBOR_Decoding_Error("CBOR item count mismatch") + return remaining + def _build_counted(self, pkt): # type: (CBOR_Packet) -> _CBORBuildResult items_data, total_items = self._build_children(pkt) @@ -1935,12 +1931,24 @@ def _collect_pair(): unknown_pairs.append((key, val_obj)) if count is CBOR_INDEFINITE: + pair_count = 0 while True: if cbor_is_break(remaining): remaining = cbor_consume_break(remaining) break + if pair_count >= config.conf.max_list_count: + raise CBOR_Decoding_Error( + "CBOR CBORF_MAP exceeded max_count=%d" + % config.conf.max_list_count + ) _collect_pair() + pair_count += 1 else: + if count > config.conf.max_list_count: + raise CBOR_Decoding_Error( + "CBOR CBORF_MAP exceeded max_count=%d" + % config.conf.max_list_count + ) for _ in range(count): _collect_pair() diff --git a/test/fields.uts b/test/fields.uts index f0092e3d62b..9b730e60900 100644 --- a/test/fields.uts +++ b/test/fields.uts @@ -2375,15 +2375,17 @@ class _EnumCopyProbe(metaclass=Enum_metaclass): assert copy(_EnumCopyProbe.A) is _EnumCopyProbe.A assert deepcopy(_EnumCopyProbe.B) is _EnumCopyProbe.B -= Mutable FieldListField defaults are isolated via Field.do_copy += Mutable PacketListField defaults isolate nested Packet members via Field.do_copy ~ core field -class _MutDefaultPkt(Packet): +from scapy.layers.inet import IP + +class _MutPktListDefault(Packet): fields_desc = [ - FieldListField("lst", [1, 2], ByteField("elt", 0)), + PacketListField("lst", [IP(dst="1.2.3.4")], IP), ] -a = _MutDefaultPkt() -b = _MutDefaultPkt() -a.lst.append(3) -assert a.lst == [1, 2, 3] -assert b.lst == [1, 2] +a = _MutPktListDefault() +b = _MutPktListDefault() +assert a.lst[0] is not b.lst[0] +a.lst[0].dst = "9.9.9.9" +assert b.lst[0].dst == "1.2.3.4" diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index 227f916c5bd..5d8197f4d94 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -1017,6 +1017,57 @@ try: except ValueError: pass += Nested REMAINDER_OF inside framed ARRAY is allowed with an outer sibling +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_ARRAY_INDEFINITE, + CBORF_REMAINDER_OF, + CBORF_TEXT_STRING, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cborpacket import CBOR_Packet + +class NestedDefinite(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_ARRAY( + CBORF_REMAINDER_OF( + "values", [], + pkt_cls=CBORF_UNSIGNED_INTEGER, + ) + ), + CBORF_TEXT_STRING("tail", ""), + ) + +class NestedIndefinite(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_ARRAY_INDEFINITE( + CBORF_REMAINDER_OF( + "values", [], + pkt_cls=CBORF_UNSIGNED_INTEGER, + ) + ), + CBORF_TEXT_STRING("tail", ""), + ) + +# Definite outer [[1, 2, 3], "tail"] +wire_def = bytes.fromhex("8283010203647461696c") +pkt = NestedDefinite(wire_def) +assert pkt.values == [1, 2, 3] +assert pkt.tail == "tail" +assert bytes(pkt) == wire_def + +# Indefinite inner array: [ [_ 1 2 3 break], "tail" ] +wire_indef = ( + b"\x82" + + b"\x9f\x01\x02\x03\xff" + + b"\x64tail" +) +pkt = NestedIndefinite(wire_indef) +assert pkt.values == [1, 2, 3] +assert pkt.tail == "tail" +# Rebuild uses indefinite encoding for the inner ARRAY_INDEFINITE schema. +assert bytes(pkt) == wire_indef + = Empty CBOR_Packet without CBOR_root is rejected by ARRAY_OF and PACKET from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_PACKET from scapy.cborpacket import CBOR_Packet @@ -3839,6 +3890,56 @@ try: except CBOR_Decoding_Error: pass += CBORF_MAP respects conf.max_list_count on definite maps +from scapy.config import conf +from scapy.cbor.cbor import CBOR_Decoding_Error +from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class LimitedMap(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_UNSIGNED_INTEGER("a", 0), + unknown_field="unknown_pairs", + ) + +# Definite map with 3 pairs: {"a":1,"b":2,"c":3} +wire = b"\xa3\x61a\x01\x61b\x02\x61c\x03" +old = conf.max_list_count +conf.max_list_count = 2 +try: + try: + LimitedMap(wire) + assert False, "definite MAP ignored conf.max_list_count" + except CBOR_Decoding_Error: + pass +finally: + conf.max_list_count = old + += CBORF_MAP respects conf.max_list_count on indefinite maps +from scapy.config import conf +from scapy.cbor.cbor import CBOR_Decoding_Error +from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class LimitedMapIndef(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_UNSIGNED_INTEGER("a", 0), + unknown_field="unknown_pairs", + ) + +# Indefinite map with 3 pairs then break +wire = b"\xbf\x61a\x01\x61b\x02\x61c\x03\xff" +old = conf.max_list_count +conf.max_list_count = 2 +try: + try: + LimitedMapIndef(wire) + assert False, "indefinite MAP ignored conf.max_list_count" + except CBOR_Decoding_Error: + pass +finally: + conf.max_list_count = old + + Medium-severity review follow-ups From 114f170fe6f5669c41b25f0e8c64ec401754dae6 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Fri, 11 Sep 2026 14:09:38 +0200 Subject: [PATCH 64/65] cbor: prefer ITEMS isinstance for remainder recursion Align remainder validation and MAP limit tests with the re-review's preferred form, including ITEMS(ARRAY(...), tail) coverage. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborfields.py | 19 ++++++++----------- test/scapy/layers/cbor.uts | 35 +++++++++++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 8f76381cfbf..dca3a85dade 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -1194,9 +1194,9 @@ def _reject_nonterminal_remainder_of(self, allow_terminal=True): "CBORF_REMAINDER_OF must be the last field " "in the sequence" ) - elif isinstance(field, _CBORF_compound) and field.CBOR_tag is None: - # Unframed ITEMS share this framing context; framed ARRAY - # establishes its own count boundary and validates itself. + elif isinstance(field, CBORF_ITEMS): + # Only unframed ITEMS share this framing context; framed + # ARRAY establishes its own count boundary. field._reject_nonterminal_remainder_of(allow_terminal=False) @@ -1930,24 +1930,21 @@ def _collect_pair(): raise CBOR_Decoding_Error(str(e)) unknown_pairs.append((key, val_obj)) + limit = config.conf.max_list_count if count is CBOR_INDEFINITE: - pair_count = 0 while True: if cbor_is_break(remaining): remaining = cbor_consume_break(remaining) break - if pair_count >= config.conf.max_list_count: + if len(seen_keys) >= limit: raise CBOR_Decoding_Error( - "CBOR CBORF_MAP exceeded max_count=%d" - % config.conf.max_list_count + "CBOR CBORF_MAP exceeded max_count=%d" % limit ) _collect_pair() - pair_count += 1 else: - if count > config.conf.max_list_count: + if count > limit: raise CBOR_Decoding_Error( - "CBOR CBORF_MAP exceeded max_count=%d" - % config.conf.max_list_count + "CBOR CBORF_MAP exceeded max_count=%d" % limit ) for _ in range(count): _collect_pair() diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index 5d8197f4d94..4e9af49acaa 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -1068,6 +1068,33 @@ assert pkt.tail == "tail" # Rebuild uses indefinite encoding for the inner ARRAY_INDEFINITE schema. assert bytes(pkt) == wire_indef += Nested REMAINDER_OF inside ARRAY under ITEMS is allowed with an outer sibling +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_ITEMS, + CBORF_REMAINDER_OF, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cborpacket import CBOR_Packet + +class NestedUnderItems(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_ARRAY( + CBORF_REMAINDER_OF( + "values", [], + pkt_cls=CBORF_UNSIGNED_INTEGER, + ) + ), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) + +# Framed array [1,2,3] then sibling unsigned 4 +wire = bytes.fromhex("8301020304") +pkt = NestedUnderItems(wire) +assert pkt.values == [1, 2, 3] +assert pkt.tail == 4 +assert bytes(pkt) == wire + = Empty CBOR_Packet without CBOR_root is rejected by ARRAY_OF and PACKET from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_PACKET from scapy.cborpacket import CBOR_Packet @@ -3902,8 +3929,8 @@ class LimitedMap(CBOR_Packet): unknown_field="unknown_pairs", ) -# Definite map with 3 pairs: {"a":1,"b":2,"c":3} -wire = b"\xa3\x61a\x01\x61b\x02\x61c\x03" +# Definite map with 3 unknown pairs (extension path grows under the limit) +wire = b"\xa3\x61x\x01\x61y\x02\x61z\x03" old = conf.max_list_count conf.max_list_count = 2 try: @@ -3927,8 +3954,8 @@ class LimitedMapIndef(CBOR_Packet): unknown_field="unknown_pairs", ) -# Indefinite map with 3 pairs then break -wire = b"\xbf\x61a\x01\x61b\x02\x61c\x03\xff" +# Indefinite map with 3 unknown pairs then break +wire = b"\xbf\x61x\x01\x61y\x02\x61z\x03\xff" old = conf.max_list_count conf.max_list_count = 2 try: From 0518a2da1ca70a330629c808f07fc7e67f52e7b9 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Fri, 11 Sep 2026 18:11:43 +0200 Subject: [PATCH 65/65] cbor: add MAP at-limit tests and nested indefinite remainder Cover conf.max_list_count exactly-at-limit for unknown MAP keys and ITEMS wrapping ARRAY_INDEFINITE(REMAINDER_OF). AI-Assisted: yes (Cursor Grok 4.6) Co-authored-by: Cursor --- scapy/cbor/cborfields.py | 6 ++-- test/fields.uts | 1 + test/scapy/layers/cbor.uts | 59 +++++++++++++++++++++++++++++++------- 3 files changed, 54 insertions(+), 12 deletions(-) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index dca3a85dade..c901d433414 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -1938,13 +1938,15 @@ def _collect_pair(): break if len(seen_keys) >= limit: raise CBOR_Decoding_Error( - "CBOR CBORF_MAP exceeded max_count=%d" % limit + "CBOR %s exceeded max_count=%d" + % (self.__class__.__name__, limit) ) _collect_pair() else: if count > limit: raise CBOR_Decoding_Error( - "CBOR CBORF_MAP exceeded max_count=%d" % limit + "CBOR %s exceeded max_count=%d" + % (self.__class__.__name__, limit) ) for _ in range(count): _collect_pair() diff --git a/test/fields.uts b/test/fields.uts index 9b730e60900..2b208f4c27e 100644 --- a/test/fields.uts +++ b/test/fields.uts @@ -2386,6 +2386,7 @@ class _MutPktListDefault(Packet): a = _MutPktListDefault() b = _MutPktListDefault() +assert a.lst is not b.lst assert a.lst[0] is not b.lst[0] a.lst[0].dst = "9.9.9.9" assert b.lst[0].dst == "1.2.3.4" diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index 4e9af49acaa..3d8b0ff91f2 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -1095,6 +1095,33 @@ assert pkt.values == [1, 2, 3] assert pkt.tail == 4 assert bytes(pkt) == wire += Nested REMAINDER_OF inside indefinite ARRAY under ITEMS is allowed with an outer sibling +from scapy.cbor.cborfields import ( + CBORF_ARRAY_INDEFINITE, + CBORF_ITEMS, + CBORF_REMAINDER_OF, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cborpacket import CBOR_Packet + +class NestedIndefUnderItems(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_ARRAY_INDEFINITE( + CBORF_REMAINDER_OF( + "values", [], + pkt_cls=CBORF_UNSIGNED_INTEGER, + ) + ), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) + +# Indefinite inner array [_ 1 2 3 break] then sibling unsigned 4 +wire = b"\x9f\x01\x02\x03\xff\x04" +pkt = NestedIndefUnderItems(wire) +assert pkt.values == [1, 2, 3] +assert pkt.tail == 4 +assert bytes(pkt) == wire + = Empty CBOR_Packet without CBOR_root is rejected by ARRAY_OF and PACKET from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_PACKET from scapy.cborpacket import CBOR_Packet @@ -3920,22 +3947,29 @@ except CBOR_Decoding_Error: = CBORF_MAP respects conf.max_list_count on definite maps from scapy.config import conf from scapy.cbor.cbor import CBOR_Decoding_Error -from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER +from scapy.cbor.cborfields import ( + CBORF_MAP, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, +) from scapy.cborpacket import CBOR_Packet class LimitedMap(CBOR_Packet): CBOR_root = CBORF_MAP( - CBORF_UNSIGNED_INTEGER("a", 0), + CBORF_optional(CBORF_UNSIGNED_INTEGER("a", 0)), unknown_field="unknown_pairs", ) -# Definite map with 3 unknown pairs (extension path grows under the limit) -wire = b"\xa3\x61x\x01\x61y\x02\x61z\x03" old = conf.max_list_count conf.max_list_count = 2 try: + # Exactly at limit: two unknown extension pairs + at_limit = LimitedMap(b"\xa2\x61x\x01\x61y\x02") + assert [k for k, _ in at_limit.unknown_pairs] == ["x", "y"] + assert [v.val for _, v in at_limit.unknown_pairs] == [1, 2] try: - LimitedMap(wire) + # Three unknown pairs exceeds conf.max_list_count + LimitedMap(b"\xa3\x61x\x01\x61y\x02\x61z\x03") assert False, "definite MAP ignored conf.max_list_count" except CBOR_Decoding_Error: pass @@ -3945,22 +3979,27 @@ finally: = CBORF_MAP respects conf.max_list_count on indefinite maps from scapy.config import conf from scapy.cbor.cbor import CBOR_Decoding_Error -from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER +from scapy.cbor.cborfields import ( + CBORF_MAP, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, +) from scapy.cborpacket import CBOR_Packet class LimitedMapIndef(CBOR_Packet): CBOR_root = CBORF_MAP( - CBORF_UNSIGNED_INTEGER("a", 0), + CBORF_optional(CBORF_UNSIGNED_INTEGER("a", 0)), unknown_field="unknown_pairs", ) -# Indefinite map with 3 unknown pairs then break -wire = b"\xbf\x61x\x01\x61y\x02\x61z\x03\xff" old = conf.max_list_count conf.max_list_count = 2 try: + at_limit = LimitedMapIndef(b"\xbf\x61x\x01\x61y\x02\xff") + assert [k for k, _ in at_limit.unknown_pairs] == ["x", "y"] + assert [v.val for _, v in at_limit.unknown_pairs] == [1, 2] try: - LimitedMapIndef(wire) + LimitedMapIndef(b"\xbf\x61x\x01\x61y\x02\x61z\x03\xff") assert False, "indefinite MAP ignored conf.max_list_count" except CBOR_Decoding_Error: pass