Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: agronholm/cbor2
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 5.6.3
Choose a base ref
...
head repository: agronholm/cbor2
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref
  • 17 commits
  • 17 files changed
  • 5 contributors

Commits on Apr 16, 2024

  1. [pre-commit.ci] pre-commit autoupdate (#230)

    updates:
    - [github.com/astral-sh/ruff-pre-commit: v0.3.5 → v0.3.7](astral-sh/ruff-pre-commit@v0.3.5...v0.3.7)
    
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    pre-commit-ci[bot] authored Apr 16, 2024
    Copy the full SHA
    e1b65f2 View commit details

Commits on Apr 23, 2024

  1. [pre-commit.ci] pre-commit autoupdate (#233)

    updates:
    - [github.com/astral-sh/ruff-pre-commit: v0.3.7 → v0.4.1](astral-sh/ruff-pre-commit@v0.3.7...v0.4.1)
    
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    pre-commit-ci[bot] authored Apr 23, 2024
    Copy the full SHA
    8794b37 View commit details

Commits on Jun 6, 2024

  1. Copy the full SHA
    a4ebd57 View commit details
  2. Copy the full SHA
    573d520 View commit details
  3. Fixed a number of compiler warnings (#239)

    * Fixed incorrect format specifiers when calling sprintf()
    * Fixed potentially uninitialized variable
    * Removed unused variables
    glaubitz authored Jun 6, 2024
    Copy the full SHA
    13681d5 View commit details
  4. Copy the full SHA
    dba7265 View commit details
  5. Bumped up the version

    agronholm committed Jun 6, 2024
    Copy the full SHA
    13b7541 View commit details

Commits on Jun 25, 2024

  1. [pre-commit.ci] pre-commit autoupdate (#241)

    updates:
    - [github.com/astral-sh/ruff-pre-commit: v0.4.8 → v0.4.10](astral-sh/ruff-pre-commit@v0.4.8...v0.4.10)
    
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    pre-commit-ci[bot] authored Jun 25, 2024
    Copy the full SHA
    9066c52 View commit details

Commits on Jul 5, 2024

  1. [pre-commit.ci] pre-commit autoupdate (#242)

    * [pre-commit.ci] pre-commit autoupdate
    
    updates:
    - [github.com/astral-sh/ruff-pre-commit: v0.4.10 → v0.5.0](astral-sh/ruff-pre-commit@v0.4.10...v0.5.0)
    - [github.com/pre-commit/mirrors-mypy: v1.10.0 → v1.10.1](pre-commit/mirrors-mypy@v1.10.0...v1.10.1)
    
    * [pre-commit.ci] auto fixes from pre-commit.com hooks
    
    for more information, see https://pre-commit.ci
    
    ---------
    
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    pre-commit-ci[bot] authored Jul 5, 2024
    Copy the full SHA
    538582f View commit details

Commits on Jul 22, 2024

  1. [pre-commit.ci] pre-commit autoupdate (#243)

    updates:
    - [github.com/astral-sh/ruff-pre-commit: v0.5.0 → v0.5.4](astral-sh/ruff-pre-commit@v0.5.0...v0.5.4)
    - [github.com/pre-commit/mirrors-mypy: v1.10.1 → v1.11.0](pre-commit/mirrors-mypy@v1.10.1...v1.11.0)
    
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    pre-commit-ci[bot] authored Jul 22, 2024
    Copy the full SHA
    0eb98db View commit details

Commits on Aug 6, 2024

  1. Updated ruff rules

    agronholm committed Aug 6, 2024
    Copy the full SHA
    53be2b5 View commit details
  2. Copy the full SHA
    7607fa1 View commit details

Commits on Sep 9, 2024

  1. [pre-commit.ci] pre-commit autoupdate (#244)

    updates:
    - [github.com/astral-sh/ruff-pre-commit: v0.5.4 → v0.6.4](astral-sh/ruff-pre-commit@v0.5.4...v0.6.4)
    - [github.com/pre-commit/mirrors-mypy: v1.11.0 → v1.11.2](pre-commit/mirrors-mypy@v1.11.0...v1.11.2)
    
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    pre-commit-ci[bot] authored Sep 9, 2024
    Copy the full SHA
    439498f View commit details

Commits on Sep 23, 2024

  1. [pre-commit.ci] pre-commit autoupdate (#245)

    updates:
    - [github.com/astral-sh/ruff-pre-commit: v0.6.4 → v0.6.7](astral-sh/ruff-pre-commit@v0.6.4...v0.6.7)
    
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    pre-commit-ci[bot] authored Sep 23, 2024
    Copy the full SHA
    85e1c02 View commit details

Commits on Oct 9, 2024

  1. Bumped up the version

    agronholm committed Oct 9, 2024
    Copy the full SHA
    6427d37 View commit details

Commits on Oct 19, 2024

  1. Copy the full SHA
    d9cee77 View commit details

Commits on Jun 5, 2025

  1. Support encoding indefinite containers (#256)

    Add the indefinite_containers parameter to the encoder functions. If the parameter is set to True, containers (maps and arrays) are encoded an indefinite containers.
    CZDanol authored Jun 5, 2025
    Copy the full SHA
    4c3c256 View commit details
30 changes: 30 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<!-- Thank you for your contribution! -->
## Changes

Fixes #. <!-- Provide issue number if exists -->

<!-- Please give a short brief about these changes. -->

## Checklist

If this is a user-facing code change, like a bugfix or a new feature, please ensure that
you've fulfilled the following conditions (where applicable):

- [ ] You've added tests (in `tests/`) added which would fail without your patch
- [ ] You've updated the documentation (in `docs/`, in case of behavior changes or new
features)
- [ ] You've added a new changelog entry (in `docs/versionhistory.rst`).

If this is a trivial change, like a typo fix or a code reformatting, then you can ignore
these instructions.

### Updating the changelog

If there are no entries after the last release, use `**UNRELEASED**` as the version.
If, say, your patch fixes issue #999, the entry should look like this:

`- Fixed big bad boo-boo in the encoder (#999
<https://github.com/agronholm/cbor2/issues/999>_; PR by @yourgithubaccount)`

If there's no issue linked, just link to your pull request instead by updating the
changelog after you've created the PR.
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -23,10 +23,10 @@ jobs:
with:
platforms: arm64
- name: Build wheels
uses: pypa/cibuildwheel@v2.16.5
uses: pypa/cibuildwheel@v2.21.3
env:
CBOR2_BUILD_C_EXTENSION: "1"
CIBW_SKIP: pp* cp36* cp37*
CIBW_SKIP: pp*
CIBW_ARCHS: auto64
CIBW_ARCHS_MACOS: x86_64 arm64
CIBW_ARCHS_LINUX: x86_64 aarch64
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -11,14 +11,14 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy-3.10"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "pypy-3.10"]
include:
- os: macos-latest
python-version: "3.8"
python-version: "3.9"
- os: macos-latest
python-version: "3.12"
- os: windows-latest
python-version: "3.8"
python-version: "3.9"
- os: windows-latest
python-version: "3.12"
runs-on: ${{ matrix.os }}
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -16,14 +16,14 @@ repos:
- id: trailing-whitespace

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.5
rev: v0.6.7
hooks:
- id: ruff
args: [--fix, --show-fixes]
- id: ruff-format

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.9.0
rev: v1.11.2
hooks:
- id: mypy
additional_dependencies:
4 changes: 2 additions & 2 deletions .readthedocs.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
version: 2

build:
os: ubuntu-22.04
os: ubuntu-24.04
tools:
python: "3.8"
python: "3.x"

sphinx:
configuration: docs/conf.py
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
@@ -54,7 +54,7 @@ Installation
Requirements
------------

* Python >= 3.8 (or `PyPy3`_ 3.8+)
* Python >= 3.9 (or `PyPy3`_ 3.9+)
* C-extension: Any C compiler that can build Python extensions.
Any modern libc with the exception of Glibc<2.9

14 changes: 7 additions & 7 deletions cbor2/_decoder.py
Original file line number Diff line number Diff line change
@@ -269,7 +269,7 @@ def _decode_length(self, subtype: int, allow_indefinite: bool = False) -> int |
elif subtype == 31 and allow_indefinite:
return None
else:
raise CBORDecodeValueError("unknown unsigned integer subtype 0x%x" % subtype)
raise CBORDecodeValueError(f"unknown unsigned integer subtype 0x{subtype:x}")

def decode_uint(self, subtype: int) -> int:
# Major tag 0
@@ -294,7 +294,7 @@ def decode_bytestring(self, subtype: int) -> bytes:
length = self._decode_length(initial_byte & 0x1F)
if length is None or length > sys.maxsize:
raise CBORDecodeValueError(
"invalid length for indefinite bytestring chunk 0x%x" % length
f"invalid length for indefinite bytestring chunk 0x{length:x}"
)
value = self.read(length)
buf.append(value)
@@ -304,7 +304,7 @@ def decode_bytestring(self, subtype: int) -> bytes:
)
else:
if length > sys.maxsize:
raise CBORDecodeValueError("invalid length for bytestring 0x%x" % length)
raise CBORDecodeValueError(f"invalid length for bytestring 0x{length:x}")
elif length <= 65536:
result = self.read(length)
else:
@@ -350,7 +350,7 @@ def decode_string(self, subtype: int) -> str:
length = self._decode_length(initial_byte & 0x1F)
if length is None or length > sys.maxsize:
raise CBORDecodeValueError(
"invalid length for indefinite string chunk 0x%x" % length
f"invalid length for indefinite string chunk 0x{length:x}"
)

try:
@@ -363,7 +363,7 @@ def decode_string(self, subtype: int) -> str:
raise CBORDecodeValueError("non-string found in indefinite length string")
else:
if length > sys.maxsize:
raise CBORDecodeValueError("invalid length for string 0x%x" % length)
raise CBORDecodeValueError(f"invalid length for string 0x{length:x}")

if length <= 65536:
try:
@@ -405,7 +405,7 @@ def decode_array(self, subtype: int) -> Sequence[Any]:
items.append(value)
else:
if length > sys.maxsize:
raise CBORDecodeValueError("invalid length for array 0x%x" % length)
raise CBORDecodeValueError(f"invalid length for array 0x{length:x}")

items = []
if not self._immutable:
@@ -476,7 +476,7 @@ def decode_special(self, subtype: int) -> Any:
return special_decoders[subtype](self)
except KeyError as e:
raise CBORDecodeValueError(
"Undefined Reserved major type 7 subtype 0x%x" % subtype
f"Undefined Reserved major type 7 subtype 0x{subtype:x}"
) from e

#
46 changes: 37 additions & 9 deletions cbor2/_encoder.py
Original file line number Diff line number Diff line change
@@ -123,6 +123,7 @@ class CBOREncoder:
"string_referencing",
"string_namespacing",
"_string_references",
"indefinite_containers",
)

_fp: IO[bytes]
@@ -138,6 +139,7 @@ def __init__(
canonical: bool = False,
date_as_datetime: bool = False,
string_referencing: bool = False,
indefinite_containers: bool = False,
):
"""
:param fp:
@@ -168,6 +170,8 @@ def __init__(
:param string_referencing:
set to ``True`` to allow more efficient serializing of repeated string
values
:param indefinite_containers:
encode containers as indefinite (use stop code instead of specifying length)
"""
self.fp = fp
@@ -177,6 +181,7 @@ def __init__(
self.value_sharing = value_sharing
self.string_referencing = string_referencing
self.string_namespacing = string_referencing
self.indefinite_containers = indefinite_containers
self.default = default
self._canonical = canonical
self._shared_containers: dict[
@@ -257,7 +262,7 @@ def canonical(self) -> bool:
return self._canonical

@contextmanager
def disable_value_sharing(self) -> Generator[None, None, None]:
def disable_value_sharing(self) -> Generator[None]:
"""
Disable value sharing in the encoder for the duration of the context
block.
@@ -268,7 +273,7 @@ def disable_value_sharing(self) -> Generator[None, None, None]:
self.value_sharing = old_value_sharing

@contextmanager
def disable_string_referencing(self) -> Generator[None, None, None]:
def disable_string_referencing(self) -> Generator[None]:
"""
Disable tracking of string references for the duration of the
context block.
@@ -279,7 +284,7 @@ def disable_string_referencing(self) -> Generator[None, None, None]:
self.string_referencing = old_string_referencing

@contextmanager
def disable_string_namespacing(self) -> Generator[None, None, None]:
def disable_string_namespacing(self) -> Generator[None]:
"""
Disable generation of new string namespaces for the duration of the
context block.
@@ -308,7 +313,7 @@ def encode(self, obj: Any) -> None:
obj_type = obj.__class__
encoder = self._encoders.get(obj_type) or self._find_encoder(obj_type) or self._default
if not encoder:
raise CBOREncodeTypeError("cannot serialize type %s" % obj_type.__name__)
raise CBOREncodeTypeError(f"cannot serialize type {obj_type.__name__}")

encoder(self, obj)

@@ -395,9 +400,11 @@ def _stringref(self, value: str | bytes) -> bool:

return False

def encode_length(self, major_tag: int, length: int) -> None:
def encode_length(self, major_tag: int, length: int | None) -> None:
major_tag <<= 5
if length < 24:
if length is None: # Indefinite
self._fp_write(struct.pack(">B", major_tag | 31))
elif length < 24:
self._fp_write(struct.pack(">B", major_tag | length))
elif length < 256:
self._fp_write(struct.pack(">BB", major_tag | 24, length))
@@ -408,6 +415,10 @@ def encode_length(self, major_tag: int, length: int) -> None:
else:
self._fp_write(struct.pack(">BQ", major_tag | 27, length))

def encode_break(self) -> None:
# Break stop code for indefinite containers
self._fp_write(struct.pack(">B", (7 << 5) | 31))

def encode_int(self, value: int) -> None:
# Big integers (2 ** 64 and over)
if value >= 18446744073709551616 or value < -18446744073709551616:
@@ -446,17 +457,23 @@ def encode_string(self, value: str) -> None:

@container_encoder
def encode_array(self, value: Sequence[Any]) -> None:
self.encode_length(4, len(value))
self.encode_length(4, len(value) if not self.indefinite_containers else None)
for item in value:
self.encode(item)

if self.indefinite_containers:
self.encode_break()

@container_encoder
def encode_map(self, value: Mapping[Any, Any]) -> None:
self.encode_length(5, len(value))
self.encode_length(5, len(value) if not self.indefinite_containers else None)
for key, val in value.items():
self.encode(key)
self.encode(val)

if self.indefinite_containers:
self.encode_break()

def encode_sortable_key(self, value: Any) -> tuple[int, bytes]:
"""
Takes a key and calculates the length of its optimal byte
@@ -471,7 +488,7 @@ def encode_sortable_key(self, value: Any) -> tuple[int, bytes]:
def encode_canonical_map(self, value: Mapping[Any, Any]) -> None:
"""Reorder keys according to Canonical CBOR specification"""
keyed_keys = ((self.encode_sortable_key(key), key, value) for key, value in value.items())
self.encode_length(5, len(value))
self.encode_length(5, len(value) if not self.indefinite_containers else None)
for sortkey, realkey, value in sorted(keyed_keys):
if self.string_referencing:
# String referencing requires that the order encoded is
@@ -482,6 +499,9 @@ def encode_canonical_map(self, value: Mapping[Any, Any]) -> None:
self._fp_write(sortkey[1])
self.encode(value)

if self.indefinite_containers:
self.encode_break()

def encode_semantic(self, value: CBORTag) -> None:
# Nested string reference domains are distinct
old_string_referencing = self.string_referencing
@@ -699,6 +719,7 @@ def dumps(
canonical: bool = False,
date_as_datetime: bool = False,
string_referencing: bool = False,
indefinite_containers: bool = False,
) -> bytes:
"""
Serialize an object to a bytestring.
@@ -730,6 +751,8 @@ def dumps(
the default behavior in previous releases (cbor2 <= 4.1.2).
:param string_referencing:
set to ``True`` to allow more efficient serializing of repeated string values
:param indefinite_containers:
encode containers as indefinite (use stop code instead of specifying length)
:return: the serialized output
"""
@@ -743,6 +766,7 @@ def dumps(
canonical=canonical,
date_as_datetime=date_as_datetime,
string_referencing=string_referencing,
indefinite_containers=indefinite_containers,
).encode(obj)
return fp.getvalue()

@@ -757,6 +781,7 @@ def dump(
canonical: bool = False,
date_as_datetime: bool = False,
string_referencing: bool = False,
indefinite_containers: bool = False,
) -> None:
"""
Serialize an object to a file.
@@ -788,6 +813,8 @@ def dump(
:param date_as_datetime:
set to ``True`` to serialize date objects as datetimes (CBOR tag 0), which was
the default behavior in previous releases (cbor2 <= 4.1.2).
:param indefinite_containers:
encode containers as indefinite (use stop code instead of specifying length)
:param string_referencing:
set to ``True`` to allow more efficient serializing of repeated string values
@@ -801,4 +828,5 @@ def dump(
canonical=canonical,
date_as_datetime=date_as_datetime,
string_referencing=string_referencing,
indefinite_containers=indefinite_containers,
).encode(obj)
4 changes: 2 additions & 2 deletions cbor2/_types.py
Original file line number Diff line number Diff line change
@@ -2,10 +2,10 @@

import threading
from collections import namedtuple
from collections.abc import Iterable, Iterator
from collections.abc import Iterable, Iterator, Mapping
from functools import total_ordering
from reprlib import recursive_repr
from typing import Any, Mapping, TypeVar
from typing import Any, TypeVar

KT = TypeVar("KT")
VT_co = TypeVar("VT_co", covariant=True)
17 changes: 16 additions & 1 deletion docs/versionhistory.rst
Original file line number Diff line number Diff line change
@@ -3,7 +3,22 @@ Version history

.. currentmodule:: cbor2

This library adheres to `Semantic Versioning <http://semver.org/>`_.
This library adheres to `Semantic Versioning <https://semver.org/>`_.

**UNRELEASED**

- Dropped support for Python 3.8
(#247 <https://github.com/agronholm/cbor2/pull/247>_; PR by @hugovk)
- Added support for encoding indefinite containers (PR by @CZDanol)

**5.6.5** (2024-10-09)

- Published binary wheels for Python 3.13

**5.6.4** (2024-06-06)

- Fixed compilation of C extension failing on GCC 14
- Fixed compiler warnings when building C extension

**5.6.3** (2024-04-11)

10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -20,13 +20,13 @@ classifiers = [
"Typing :: Typed",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
requires-python = ">= 3.8"
requires-python = ">= 3.9"
dynamic = ["version"]

[project.urls]
@@ -62,13 +62,13 @@ include = ["cbor2"]
line-length = 99

[tool.ruff.lint]
select = [
"E", "F", "W", # default flake-8
extend-select = [
"I", # isort
"ISC", # flake8-implicit-str-concat
"PGH", # pygrep-hooks
"RUF100", # unused noqa (yesqa)
"UP", # pyupgrade
"W", # pycodestyle warnings
]
ignore = ["ISC001"]

@@ -89,7 +89,7 @@ show_missing = true
[tool.tox]
legacy_tox_ini = """
[tox]
envlist = py38, py39, py310, py311, py312, pypy3
envlist = py39, py310, py311, py312, py313, pypy3
skip_missing_interpreters = true
minversion = 4.0.0
14 changes: 5 additions & 9 deletions source/decoder.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <datetime.h>
#include <inttypes.h>
#include <string.h>
#include <stdbool.h>
#include <limits.h>
@@ -348,7 +349,7 @@ _CBORDecoder_get_immutable(CBORDecoderObject *self, void *closure)

// Utility functions /////////////////////////////////////////////////////////

static int
static void
raise_from(PyObject *new_exc_type, const char *message) {
// This requires the error indicator to be set
PyObject *cause;
@@ -694,7 +695,7 @@ decode_bytestring(CBORDecoderObject *self, uint8_t subtype)
return NULL;

if (length > (uint64_t)PY_SSIZE_T_MAX - (uint64_t)PyBytesObject_SIZE) {
sprintf(length_hex, "%llX", length);
sprintf(length_hex, "%" PRIX64, length);
PyErr_Format(
_CBOR2_CBORDecodeValueError,
"excessive bytestring size 0x%s", length_hex);
@@ -894,7 +895,7 @@ decode_string(CBORDecoderObject *self, uint8_t subtype)
if (decode_length(self, subtype, &length, &indefinite) == -1)
return NULL;
if (length > (uint64_t)PY_SSIZE_T_MAX - (uint64_t)PyBytesObject_SIZE) {
sprintf(length_hex, "%llX", length);
sprintf(length_hex, "%" PRIX64, length);
PyErr_Format(
_CBOR2_CBORDecodeValueError,
"excessive string size 0x%s", length_hex);
@@ -1057,7 +1058,7 @@ decode_array(CBORDecoderObject *self, uint8_t subtype)
if (indefinite)
return decode_indefinite_array(self);
if (length > (uint64_t)PY_SSIZE_T_MAX) {
sprintf(length_hex, "%llX", length);
sprintf(length_hex, "%" PRIX64, length);
PyErr_Format(
_CBOR2_CBORDecodeValueError,
"excessive array size 0x%s", length_hex);
@@ -1253,12 +1254,7 @@ parse_datetimestr(CBORDecoderObject *self, PyObject *str)
(offset_sign ? -1 : 1) *
(offset_H * 3600 + offset_M * 60), 0);
if (delta) {
#if PY_VERSION_HEX >= 0x03070000
tz = PyTimeZone_FromOffset(delta);
#else
tz = PyObject_CallFunctionObjArgs(
_CBOR2_timezone, delta, NULL);
#endif
Py_DECREF(delta);
}
} else
111 changes: 93 additions & 18 deletions source/encoder.c
Original file line number Diff line number Diff line change
@@ -113,6 +113,7 @@ CBOREncoder_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
self->shared_handler = NULL;
self->string_referencing = false;
self->string_namespacing = false;
self->indefinite_containers = false;
}
return (PyObject *) self;
}
@@ -126,16 +127,16 @@ CBOREncoder_init(CBOREncoderObject *self, PyObject *args, PyObject *kwargs)
{
static char *keywords[] = {
"fp", "datetime_as_timestamp", "timezone", "value_sharing", "default",
"canonical", "date_as_datetime", "string_referencing", NULL
"canonical", "date_as_datetime", "string_referencing", "indefinite_containers", NULL
};
PyObject *tmp, *fp = NULL, *default_handler = NULL, *tz = NULL;
int value_sharing = 0, timestamp_format = 0, enc_style = 0,
date_as_datetime = 0, string_referencing = 0;
date_as_datetime = 0, string_referencing = 0, indefinite_containers = 0;

if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|pOpOppp", keywords,
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|pOpOpppp", keywords,
&fp, &timestamp_format, &tz, &value_sharing,
&default_handler, &enc_style, &date_as_datetime,
&string_referencing))
&string_referencing, &indefinite_containers))
return -1;
// Predicate values are returned as ints, but need to be stored as bool or ubyte
if (timestamp_format == 1)
@@ -150,6 +151,8 @@ CBOREncoder_init(CBOREncoderObject *self, PyObject *args, PyObject *kwargs)
self->string_referencing = true;
self->string_namespacing = true;
}
if (indefinite_containers == 1)
self->indefinite_containers = true;


if (_CBOREncoder_set_fp(self, fp, NULL) == -1)
@@ -345,17 +348,19 @@ CBOREncoder_write(CBOREncoderObject *self, PyObject *data)
Py_RETURN_NONE;
}


static int
encode_length(CBOREncoderObject *self, const uint8_t major_tag,
const uint64_t length)
encode_length_possibly_indefinite(CBOREncoderObject *self, const uint8_t major_tag,
const uint64_t length, const bool indefinite)
{
LeadByte *lead;
char buf[sizeof(LeadByte) + sizeof(uint64_t)];

lead = (LeadByte*)buf;
lead->major = major_tag;
if (length < 24) {
if (indefinite) {
lead->subtype = 31;
return fp_write(self, buf, 1);
} else if (length < 24) {
lead->subtype = (uint8_t) length;
return fp_write(self, buf, 1);
} else if (length <= UCHAR_MAX) {
@@ -377,18 +382,75 @@ encode_length(CBOREncoderObject *self, const uint8_t major_tag,
}
}

static int
encode_length(CBOREncoderObject *self, const uint8_t major_tag,
const uint64_t length) {
return encode_length_possibly_indefinite(self, major_tag, length, false);
}

typedef struct {
uint64_t value;
bool is_none;
} UInt64OrNone;

static int uint64_or_none(PyObject *obj, void *param) {
if (obj == Py_None) {
const UInt64OrNone result = {
.value = 0,
.is_none = true,
};
*((UInt64OrNone*)param) = result;
return 1;

} else if (PyLong_Check(obj)) {
const uint64_t val = PyLong_AsUnsignedLong(obj);
if (PyErr_Occurred()) {
return 0;
}

const UInt64OrNone result = {
.value = val,
.is_none = false,
};
*((UInt64OrNone*)param) = result;
return 1;

} else {
PyErr_SetString(PyExc_TypeError, "must be int or None");
return 0;
}
}

// CBOREncoder.encode_length(self, major_tag, length)
static PyObject *
CBOREncoder_encode_length(CBOREncoderObject *self, PyObject *args)
{
uint8_t major_tag;
uint64_t length;
UInt64OrNone length;

if (!PyArg_ParseTuple(args, "BK", &major_tag, &length))
if (!PyArg_ParseTuple(args, "BO&", &major_tag, &uint64_or_none, &length))
return NULL;
if (encode_length_possibly_indefinite(self, major_tag, length.value, length.is_none) == -1)
return NULL;
if (encode_length(self, major_tag, length) == -1)
Py_RETURN_NONE;
}

static int
encode_break(CBOREncoderObject *self)
{
LeadByte lead;
lead.major = 7;
lead.subtype = 31;
return fp_write(self, (const char*) &lead, 1);
}

// CBOREncoder.encode_break(self)
static PyObject *
CBOREncoder_encode_break(CBOREncoderObject *self)
{
if (encode_break(self) == -1) {
return NULL;
}
Py_RETURN_NONE;
}

@@ -761,7 +823,7 @@ encode_array(CBOREncoderObject *self, PyObject *value)
if (fast) {
length = PySequence_Fast_GET_SIZE(fast);
items = PySequence_Fast_ITEMS(fast);
if (encode_length(self, 4, length) == 0) {
if (encode_length_possibly_indefinite(self, 4, length, self->indefinite_containers) == 0) {
while (length) {
ret = CBOREncoder_encode(self, *items);
if (ret)
@@ -774,6 +836,9 @@ encode_array(CBOREncoderObject *self, PyObject *value)
Py_INCREF(Py_None);
ret = Py_None;
}
if (self->indefinite_containers && encode_break(self) == -1) {
goto error;
}
error:
Py_DECREF(fast);
}
@@ -796,7 +861,7 @@ encode_dict(CBOREncoderObject *self, PyObject *value)
PyObject *key, *val, *ret;
Py_ssize_t pos = 0;

if (encode_length(self, 5, PyDict_Size(value)) == 0) {
if (encode_length_possibly_indefinite(self, 5, PyDict_Size(value), self->indefinite_containers) == 0) {
while (PyDict_Next(value, &pos, &key, &val)) {
Py_INCREF(key);
ret = CBOREncoder_encode(self, key);
@@ -813,7 +878,11 @@ encode_dict(CBOREncoderObject *self, PyObject *value)
else
return NULL;
}
if (self->indefinite_containers && encode_break(self) == -1) {
return NULL;
}
}

Py_RETURN_NONE;
}

@@ -830,7 +899,7 @@ encode_mapping(CBOREncoderObject *self, PyObject *value)
if (fast) {
length = PySequence_Fast_GET_SIZE(fast);
items = PySequence_Fast_ITEMS(fast);
if (encode_length(self, 5, length) == 0) {
if (encode_length_possibly_indefinite(self, 5, length, self->indefinite_containers) == 0) {
while (length) {
ret = CBOREncoder_encode(self, PyTuple_GET_ITEM(*items, 0));
if (ret)
@@ -845,6 +914,9 @@ encode_mapping(CBOREncoderObject *self, PyObject *value)
items++;
length--;
}
if (self->indefinite_containers && encode_break(self) == -1) {
goto error;
}
ret = Py_None;
Py_INCREF(ret);
}
@@ -1040,8 +1112,6 @@ CBOREncoder_encode_date(CBOREncoderObject *self, PyObject *value)
// semantic type 100 or 1004

PyObject *tmp, *ret = NULL;
const char *buf;
Py_ssize_t length;
if (self->date_as_datetime) {
tmp = PyDateTimeAPI->DateTime_FromDateAndTime(
PyDateTime_GET_YEAR(value),
@@ -1120,7 +1190,7 @@ decimal_negative(PyObject *value)
static PyObject *
encode_decimal_digits(CBOREncoderObject *self, PyObject *value)
{
PyObject *tuple, *digits, *exp, *sig, *ten, *tmp, *ret = NULL;
PyObject *tuple, *digits, *exp, *sig, *ten, *tmp = NULL, *ret = NULL;
int sign = 0;
bool sharing;

@@ -1730,7 +1800,7 @@ encode_canonical_map_list(CBOREncoderObject *self, PyObject *list)

if (PyList_Sort(list) == -1)
return NULL;
if (encode_length(self, 5, PyList_GET_SIZE(list)) == -1)
if (encode_length_possibly_indefinite(self, 5, PyList_GET_SIZE(list), self->indefinite_containers) == -1)
return NULL;
for (index = 0; index < PyList_GET_SIZE(list); ++index) {
// If we are encoding string references, the order of the keys
@@ -1755,6 +1825,9 @@ encode_canonical_map_list(CBOREncoderObject *self, PyObject *list)
else
return NULL;
}
if (self->indefinite_containers && encode_break(self) == -1) {
return NULL;
}
Py_RETURN_NONE;
}

@@ -2116,6 +2189,8 @@ static PyMethodDef CBOREncoder_methods[] = {
{"encode_length", (PyCFunction) CBOREncoder_encode_length, METH_VARARGS,
"encode the specified *major_tag* with the specified *length* to "
"the output"},
{"encode_break", (PyCFunction) CBOREncoder_encode_break, METH_NOARGS,
"encode break stop code for indefinite containers"},
{"encode_int", (PyCFunction) CBOREncoder_encode_int, METH_O,
"encode the specified integer *value* to the output"},
{"encode_float", (PyCFunction) CBOREncoder_encode_float, METH_O,
1 change: 1 addition & 0 deletions source/encoder.h
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ typedef struct {
bool value_sharing;
bool string_referencing;
bool string_namespacing;
bool indefinite_containers;
} CBOREncoderObject;

extern PyTypeObject CBOREncoderType;
21 changes: 0 additions & 21 deletions source/module.c
Original file line number Diff line number Diff line change
@@ -525,31 +525,10 @@ _CBOR2_init_re_compile(void)
int
_CBOR2_init_timezone_utc(void)
{
#if PY_VERSION_HEX >= 0x03070000
Py_INCREF(PyDateTime_TimeZone_UTC);
_CBOR2_timezone_utc = PyDateTime_TimeZone_UTC;
_CBOR2_timezone = NULL;
return 0;
#else
PyObject* datetime;

// from datetime import timezone
// utc = timezone.utc
datetime = PyImport_ImportModule("datetime");
if (!datetime)
goto error;
_CBOR2_timezone = PyObject_GetAttr(datetime, _CBOR2_str_timezone);
Py_DECREF(datetime);
if (!_CBOR2_timezone)
goto error;
_CBOR2_timezone_utc = PyObject_GetAttr(_CBOR2_timezone, _CBOR2_str_utc);
if (!_CBOR2_timezone_utc)
goto error;
return 0;
error:
PyErr_SetString(PyExc_ImportError, "unable to import timezone from datetime");
return -1;
#endif
}


8 changes: 4 additions & 4 deletions tests/test_decoder.py
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@
from io import BytesIO
from ipaddress import ip_address, ip_network
from pathlib import Path
from typing import Type, cast
from typing import cast
from uuid import UUID

import pytest
@@ -831,14 +831,14 @@ def test_load_from_file(impl, tmpdir):

def test_nested_dict(impl):
value = impl.loads(unhexlify("A1D9177082010201"))
assert type(value) is dict # noqa: E721
assert type(value) is dict
assert value == {impl.CBORTag(6000, (1, 2)): 1}


def test_set(impl):
payload = unhexlify("d9010283616361626161")
value = impl.loads(payload)
assert type(value) is set # noqa: E721
assert type(value) is set
assert value == {"a", "b", "c"}


@@ -955,7 +955,7 @@ def test_decimal_payload_unpacking(impl, data, expected):
],
)
def test_oversized_read(impl, payload: bytes, tmp_path: Path) -> None:
CBORDecodeEOF = cast(Type[Exception], getattr(impl, "CBORDecodeEOF"))
CBORDecodeEOF = cast(type[Exception], getattr(impl, "CBORDecodeEOF"))
with pytest.raises(CBORDecodeEOF, match="premature end of stream"):
dummy_path = tmp_path / "testdata"
dummy_path.write_bytes(payload)
57 changes: 52 additions & 5 deletions tests/test_encoder.py
Original file line number Diff line number Diff line change
@@ -78,11 +78,42 @@ def test_encoders_load_type(impl):


def test_encode_length(impl):
# This test is purely for coverage in the C variant
with BytesIO() as stream:
encoder = impl.CBOREncoder(stream)
encoder.encode_length(0, 1)
assert stream.getvalue() == b"\x01"
fp = None
encoder = None

def reset_encoder():
nonlocal fp, encoder
fp = BytesIO()
encoder = impl.CBOREncoder(fp)

reset_encoder()
encoder.encode_length(0, 1)
assert fp.getvalue() == b"\x01"

# Array of size 2
reset_encoder()
encoder.encode_length(4, 2)
assert fp.getvalue() == b"\x82"

# Array of indefinite size
reset_encoder()
encoder.encode_length(4, None)
assert fp.getvalue() == b"\x9f"

# Map of size 0
reset_encoder()
encoder.encode_length(5, 0)
assert fp.getvalue() == b"\xa0"

# Map of indefinite size
reset_encoder()
encoder.encode_length(5, None)
assert fp.getvalue() == b"\xbf"

# Indefinite container break
reset_encoder()
encoder.encode_break()
assert fp.getvalue() == b"\xff"


def test_canonical_attr(impl):
@@ -654,3 +685,19 @@ def test_invariant_encode_decode(impl, val):
undergoing an encode and decode)
"""
assert impl.loads(impl.dumps(val)) == val


def test_indefinite_containers(impl):
expected = b"\x82\x00\x01"
assert impl.dumps([0, 1]) == expected

expected = b"\x9f\x00\x01\xff"
assert impl.dumps([0, 1], indefinite_containers=True) == expected
assert impl.dumps([0, 1], indefinite_containers=True, canonical=True) == expected

expected = b"\xa0"
assert impl.dumps({}) == expected

expected = b"\xbf\xff"
assert impl.dumps({}, indefinite_containers=True) == expected
assert impl.dumps({}, indefinite_containers=True, canonical=True) == expected