lodum.cbor

  1# SPDX-FileCopyrightText: 2025-present Michael R. Bernstein <zopemaven@gmail.com>
  2#
  3# SPDX-License-Identifier: Apache-2.0
  4try:
  5    import cbor2
  6except ImportError:
  7    cbor2 = None  # type: ignore
  8from typing import Any, Iterator, Type, TypeVar
  9
 10from .core import Loader, BaseDumper, BaseLoader
 11from .exception import DeserializationError
 12from .internal import dump, load, DEFAULT_MAX_SIZE
 13
 14T = TypeVar("T")
 15
 16# --- Public API ---
 17
 18
 19def dumps(obj: Any) -> bytes:
 20    """
 21    Encodes a Python object to CBOR bytes.
 22
 23    Args:
 24        obj: The object to encode. Must be lodum-enabled or a supported type.
 25
 26    Returns:
 27        The CBOR-encoded bytes.
 28
 29    Raises:
 30        ImportError: If cbor2 is not installed.
 31    """
 32    if cbor2 is None:
 33        raise ImportError(
 34            "cbor2 is required for CBOR serialization. Install it with 'pip install lodum[cbor]'."
 35        )
 36    dumper = CborDumper()
 37    dumped_data = dump(obj, dumper)
 38    return cbor2.dumps(dumped_data)
 39
 40
 41def loads(cls: Type[T], cbor_bytes: bytes, max_size: int = DEFAULT_MAX_SIZE) -> T:
 42    """
 43    Decodes CBOR bytes into a Python object of the specified type.
 44
 45    Args:
 46        cls: The class to instantiate.
 47        cbor_bytes: The CBOR data to decode.
 48        max_size: Maximum allowed size of the input bytes.
 49
 50    Returns:
 51        An instance of cls populated with the decoded data.
 52
 53    Raises:
 54        DeserializationError: If the input is invalid or exceeds max_size.
 55        ImportError: If cbor2 is not installed.
 56    """
 57    if len(cbor_bytes) > max_size:
 58        raise DeserializationError(
 59            f"Input size ({len(cbor_bytes)}) exceeds maximum allowed ({max_size})"
 60        )
 61
 62    if cbor2 is None:
 63        raise ImportError(
 64            "cbor2 is required for CBOR deserialization. Install it with 'pip install lodum[cbor]'."
 65        )
 66    try:
 67        data = cbor2.loads(cbor_bytes)
 68    except Exception as e:
 69        raise DeserializationError(f"Failed to parse CBOR: {e}")
 70    loader = CborLoader(data)
 71    return load(cls, loader)
 72
 73
 74# --- CBOR Dumper Implementation ---
 75
 76
 77class CborDumper(BaseDumper):
 78    pass
 79
 80
 81# --- CBOR Loader Implementation ---
 82
 83
 84class CborLoader(BaseLoader):
 85    def load_int(self) -> int:
 86        if not isinstance(self._data, int):
 87            raise DeserializationError(f"Expected int, got {type(self._data).__name__}")
 88        return self._data
 89
 90    def load_str(self) -> str:
 91        if not isinstance(self._data, str):
 92            raise DeserializationError(f"Expected str, got {type(self._data).__name__}")
 93        return self._data
 94
 95    def load_float(self) -> float:
 96        if not isinstance(self._data, (float, int)):
 97            raise DeserializationError(
 98                f"Expected float, got {type(self._data).__name__}"
 99            )
100        return float(self._data)
101
102    def load_bool(self) -> bool:
103        if not isinstance(self._data, bool):
104            raise DeserializationError(
105                f"Expected bool, got {type(self._data).__name__}"
106            )
107        return self._data
108
109    def load_list(self) -> Iterator["Loader"]:
110        if not isinstance(self._data, list):
111            raise DeserializationError(
112                f"Expected list, got {type(self._data).__name__}"
113            )
114        return (CborLoader(item) for item in self._data)
115
116    def load_dict(self) -> Iterator[tuple[str, "Loader"]]:
117        if not isinstance(self._data, dict):
118            raise DeserializationError(
119                f"Expected dict, got {type(self._data).__name__}"
120            )
121        return ((k, CborLoader(v)) for k, v in self._data.items())
122
123    def load_bytes_value(self, value: Any) -> bytes:
124        if not isinstance(value, bytes):
125            raise DeserializationError(f"Expected bytes, got {type(value).__name__}")
126        return value
def dumps(obj: Any) -> bytes:
20def dumps(obj: Any) -> bytes:
21    """
22    Encodes a Python object to CBOR bytes.
23
24    Args:
25        obj: The object to encode. Must be lodum-enabled or a supported type.
26
27    Returns:
28        The CBOR-encoded bytes.
29
30    Raises:
31        ImportError: If cbor2 is not installed.
32    """
33    if cbor2 is None:
34        raise ImportError(
35            "cbor2 is required for CBOR serialization. Install it with 'pip install lodum[cbor]'."
36        )
37    dumper = CborDumper()
38    dumped_data = dump(obj, dumper)
39    return cbor2.dumps(dumped_data)

Encodes a Python object to CBOR bytes.

Args: obj: The object to encode. Must be lodum-enabled or a supported type.

Returns: The CBOR-encoded bytes.

Raises: ImportError: If cbor2 is not installed.

def loads(cls: Type[~T], cbor_bytes: bytes, max_size: int = 10485760) -> ~T:
42def loads(cls: Type[T], cbor_bytes: bytes, max_size: int = DEFAULT_MAX_SIZE) -> T:
43    """
44    Decodes CBOR bytes into a Python object of the specified type.
45
46    Args:
47        cls: The class to instantiate.
48        cbor_bytes: The CBOR data to decode.
49        max_size: Maximum allowed size of the input bytes.
50
51    Returns:
52        An instance of cls populated with the decoded data.
53
54    Raises:
55        DeserializationError: If the input is invalid or exceeds max_size.
56        ImportError: If cbor2 is not installed.
57    """
58    if len(cbor_bytes) > max_size:
59        raise DeserializationError(
60            f"Input size ({len(cbor_bytes)}) exceeds maximum allowed ({max_size})"
61        )
62
63    if cbor2 is None:
64        raise ImportError(
65            "cbor2 is required for CBOR deserialization. Install it with 'pip install lodum[cbor]'."
66        )
67    try:
68        data = cbor2.loads(cbor_bytes)
69    except Exception as e:
70        raise DeserializationError(f"Failed to parse CBOR: {e}")
71    loader = CborLoader(data)
72    return load(cls, loader)

Decodes CBOR bytes into a Python object of the specified type.

Args: cls: The class to instantiate. cbor_bytes: The CBOR data to decode. max_size: Maximum allowed size of the input bytes.

Returns: An instance of cls populated with the decoded data.

Raises: DeserializationError: If the input is invalid or exceeds max_size. ImportError: If cbor2 is not installed.

class CborDumper(lodum.core.BaseDumper):
78class CborDumper(BaseDumper):
79    pass

Base implementation of the Dumper protocol to reduce duplication.

class CborLoader(lodum.core.BaseLoader):
 85class CborLoader(BaseLoader):
 86    def load_int(self) -> int:
 87        if not isinstance(self._data, int):
 88            raise DeserializationError(f"Expected int, got {type(self._data).__name__}")
 89        return self._data
 90
 91    def load_str(self) -> str:
 92        if not isinstance(self._data, str):
 93            raise DeserializationError(f"Expected str, got {type(self._data).__name__}")
 94        return self._data
 95
 96    def load_float(self) -> float:
 97        if not isinstance(self._data, (float, int)):
 98            raise DeserializationError(
 99                f"Expected float, got {type(self._data).__name__}"
100            )
101        return float(self._data)
102
103    def load_bool(self) -> bool:
104        if not isinstance(self._data, bool):
105            raise DeserializationError(
106                f"Expected bool, got {type(self._data).__name__}"
107            )
108        return self._data
109
110    def load_list(self) -> Iterator["Loader"]:
111        if not isinstance(self._data, list):
112            raise DeserializationError(
113                f"Expected list, got {type(self._data).__name__}"
114            )
115        return (CborLoader(item) for item in self._data)
116
117    def load_dict(self) -> Iterator[tuple[str, "Loader"]]:
118        if not isinstance(self._data, dict):
119            raise DeserializationError(
120                f"Expected dict, got {type(self._data).__name__}"
121            )
122        return ((k, CborLoader(v)) for k, v in self._data.items())
123
124    def load_bytes_value(self, value: Any) -> bytes:
125        if not isinstance(value, bytes):
126            raise DeserializationError(f"Expected bytes, got {type(value).__name__}")
127        return value

Base implementation of the Loader protocol to reduce duplication.

def load_int(self) -> int:
86    def load_int(self) -> int:
87        if not isinstance(self._data, int):
88            raise DeserializationError(f"Expected int, got {type(self._data).__name__}")
89        return self._data
def load_str(self) -> str:
91    def load_str(self) -> str:
92        if not isinstance(self._data, str):
93            raise DeserializationError(f"Expected str, got {type(self._data).__name__}")
94        return self._data
def load_float(self) -> float:
 96    def load_float(self) -> float:
 97        if not isinstance(self._data, (float, int)):
 98            raise DeserializationError(
 99                f"Expected float, got {type(self._data).__name__}"
100            )
101        return float(self._data)
def load_bool(self) -> bool:
103    def load_bool(self) -> bool:
104        if not isinstance(self._data, bool):
105            raise DeserializationError(
106                f"Expected bool, got {type(self._data).__name__}"
107            )
108        return self._data
def load_list(self) -> Iterator[lodum.core.Loader]:
110    def load_list(self) -> Iterator["Loader"]:
111        if not isinstance(self._data, list):
112            raise DeserializationError(
113                f"Expected list, got {type(self._data).__name__}"
114            )
115        return (CborLoader(item) for item in self._data)
def load_dict(self) -> Iterator[tuple[str, lodum.core.Loader]]:
117    def load_dict(self) -> Iterator[tuple[str, "Loader"]]:
118        if not isinstance(self._data, dict):
119            raise DeserializationError(
120                f"Expected dict, got {type(self._data).__name__}"
121            )
122        return ((k, CborLoader(v)) for k, v in self._data.items())
def load_bytes_value(self, value: Any) -> bytes:
124    def load_bytes_value(self, value: Any) -> bytes:
125        if not isinstance(value, bytes):
126            raise DeserializationError(f"Expected bytes, got {type(value).__name__}")
127        return value