pyccobs 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyccobs-0.1.0/CMakeLists.txt +13 -0
- pyccobs-0.1.0/LICENSE +15 -0
- pyccobs-0.1.0/MANIFEST.in +6 -0
- pyccobs-0.1.0/PKG-INFO +71 -0
- pyccobs-0.1.0/README.md +55 -0
- pyccobs-0.1.0/pyproject.toml +49 -0
- pyccobs-0.1.0/setup.cfg +4 -0
- pyccobs-0.1.0/setup.py +85 -0
- pyccobs-0.1.0/src/ccobs/__init__.py +312 -0
- pyccobs-0.1.0/src/ccobs/_lib.py +131 -0
- pyccobs-0.1.0/src/pyccobs.egg-info/PKG-INFO +71 -0
- pyccobs-0.1.0/src/pyccobs.egg-info/SOURCES.txt +19 -0
- pyccobs-0.1.0/src/pyccobs.egg-info/dependency_links.txt +1 -0
- pyccobs-0.1.0/src/pyccobs.egg-info/requires.txt +3 -0
- pyccobs-0.1.0/src/pyccobs.egg-info/top_level.txt +1 -0
- pyccobs-0.1.0/tests/test_ccobs.py +89 -0
- pyccobs-0.1.0/vendor/ccobs/CMakeLists.txt +42 -0
- pyccobs-0.1.0/vendor/ccobs/LICENSE +12 -0
- pyccobs-0.1.0/vendor/ccobs/README.md +82 -0
- pyccobs-0.1.0/vendor/ccobs/include/ccobs.h +140 -0
- pyccobs-0.1.0/vendor/ccobs/src/cobs.c +405 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.15)
|
|
2
|
+
project(pyccobs LANGUAGES C)
|
|
3
|
+
|
|
4
|
+
set(BUILD_SHARED_LIBS ON CACHE BOOL "Build ccobs as a shared library" FORCE)
|
|
5
|
+
set(BUILD_TESTING OFF CACHE BOOL "Do not build ccobs's C tests for the wheel" FORCE)
|
|
6
|
+
|
|
7
|
+
add_subdirectory(vendor/ccobs)
|
|
8
|
+
|
|
9
|
+
install(
|
|
10
|
+
TARGETS ccobs
|
|
11
|
+
LIBRARY DESTINATION ccobs
|
|
12
|
+
RUNTIME DESTINATION ccobs
|
|
13
|
+
)
|
pyccobs-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Zero-Clause BSD
|
|
2
|
+
|
|
3
|
+
Copyright (C) 2026 Elizabeth Kiara Regina Ashford
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
9
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
10
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
11
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
12
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
13
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
14
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
15
|
+
|
pyccobs-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyccobs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Portable ctypes bindings for ccobs
|
|
5
|
+
Author: Elizabeth Kiara Regina Ashford
|
|
6
|
+
License-Expression: 0BSD
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: C
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
License-File: vendor/ccobs/LICENSE
|
|
13
|
+
Provides-Extra: test
|
|
14
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# pyccobs
|
|
18
|
+
|
|
19
|
+
`pyccobs` provides Python bindings to [ccobs](https://github.com/Elizafox/ccobs)
|
|
20
|
+
using only the standard library's `ctypes` module. The ccobs C library is
|
|
21
|
+
vendored and built into the package, so a separate ccobs installation is not
|
|
22
|
+
required. It includes one-shot and streaming APIs as well as exposing the
|
|
23
|
+
native library handle.
|
|
24
|
+
|
|
25
|
+
## Build and use
|
|
26
|
+
|
|
27
|
+
Python 3.11+ and CMake 3.15+ are required.
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
python -m pip install -e .
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The package build invokes the vendored ccobs CMake project directly. Because
|
|
34
|
+
the package uses `ctypes` rather than the CPython extension API, the resulting
|
|
35
|
+
wheel is tagged for Python 3 generally (for example, `py3-none-win_amd64`).
|
|
36
|
+
|
|
37
|
+
Set `CCOBS_LIBRARY_PATH` to a compatible shared library (or its directory) to
|
|
38
|
+
override the bundled copy when testing a different ccobs build.
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import ccobs
|
|
42
|
+
|
|
43
|
+
wire = ccobs.encode(b"hello\0world")
|
|
44
|
+
assert ccobs.decode(wire) == b"hello\0world"
|
|
45
|
+
|
|
46
|
+
batch = ccobs.encodev([b"first", b"", b"third"])
|
|
47
|
+
assert batch == b"".join(ccobs.encode(item) for item in [b"first", b"", b"third"])
|
|
48
|
+
|
|
49
|
+
encoder = ccobs.Encoder()
|
|
50
|
+
wire = encoder.feed(b"hello ") + encoder.feed(b"world", end_message=True)
|
|
51
|
+
wire += encoder.feedv([b"second", b"third"])
|
|
52
|
+
encoder.close()
|
|
53
|
+
|
|
54
|
+
decoder = ccobs.Decoder()
|
|
55
|
+
decoded, used, frame_sizes = decoder.feedv(wire)
|
|
56
|
+
decoder.close()
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`Decoder.feed()` returns `(decoded_bytes, consumed_bytes, message_complete)`
|
|
60
|
+
and stops after the first complete frame. `Decoder.feedv()` returns
|
|
61
|
+
`(decoded_bytes, consumed_bytes, frame_sizes)` and
|
|
62
|
+
decodes every complete frame in the supplied chunk in one `ccobs_decodev()`
|
|
63
|
+
call. `frame_sizes` gives the decoded size of each completed frame in the flat
|
|
64
|
+
output; a size may include bytes returned by an earlier partial call.
|
|
65
|
+
`Encoder.feedv()` encodes a batch of complete messages through the native
|
|
66
|
+
streaming vector API. Both stateful classes are context managers and have
|
|
67
|
+
`reset()` and `close()`.
|
|
68
|
+
|
|
69
|
+
For direct access to `ccobs_create`, `ccobs_encodev`, and the rest of the C API,
|
|
70
|
+
use `ccobs.load_library()`. Its functions have their ctypes `argtypes` and
|
|
71
|
+
`restype` declarations installed.
|
pyccobs-0.1.0/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# pyccobs
|
|
2
|
+
|
|
3
|
+
`pyccobs` provides Python bindings to [ccobs](https://github.com/Elizafox/ccobs)
|
|
4
|
+
using only the standard library's `ctypes` module. The ccobs C library is
|
|
5
|
+
vendored and built into the package, so a separate ccobs installation is not
|
|
6
|
+
required. It includes one-shot and streaming APIs as well as exposing the
|
|
7
|
+
native library handle.
|
|
8
|
+
|
|
9
|
+
## Build and use
|
|
10
|
+
|
|
11
|
+
Python 3.11+ and CMake 3.15+ are required.
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
python -m pip install -e .
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The package build invokes the vendored ccobs CMake project directly. Because
|
|
18
|
+
the package uses `ctypes` rather than the CPython extension API, the resulting
|
|
19
|
+
wheel is tagged for Python 3 generally (for example, `py3-none-win_amd64`).
|
|
20
|
+
|
|
21
|
+
Set `CCOBS_LIBRARY_PATH` to a compatible shared library (or its directory) to
|
|
22
|
+
override the bundled copy when testing a different ccobs build.
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import ccobs
|
|
26
|
+
|
|
27
|
+
wire = ccobs.encode(b"hello\0world")
|
|
28
|
+
assert ccobs.decode(wire) == b"hello\0world"
|
|
29
|
+
|
|
30
|
+
batch = ccobs.encodev([b"first", b"", b"third"])
|
|
31
|
+
assert batch == b"".join(ccobs.encode(item) for item in [b"first", b"", b"third"])
|
|
32
|
+
|
|
33
|
+
encoder = ccobs.Encoder()
|
|
34
|
+
wire = encoder.feed(b"hello ") + encoder.feed(b"world", end_message=True)
|
|
35
|
+
wire += encoder.feedv([b"second", b"third"])
|
|
36
|
+
encoder.close()
|
|
37
|
+
|
|
38
|
+
decoder = ccobs.Decoder()
|
|
39
|
+
decoded, used, frame_sizes = decoder.feedv(wire)
|
|
40
|
+
decoder.close()
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`Decoder.feed()` returns `(decoded_bytes, consumed_bytes, message_complete)`
|
|
44
|
+
and stops after the first complete frame. `Decoder.feedv()` returns
|
|
45
|
+
`(decoded_bytes, consumed_bytes, frame_sizes)` and
|
|
46
|
+
decodes every complete frame in the supplied chunk in one `ccobs_decodev()`
|
|
47
|
+
call. `frame_sizes` gives the decoded size of each completed frame in the flat
|
|
48
|
+
output; a size may include bytes returned by an earlier partial call.
|
|
49
|
+
`Encoder.feedv()` encodes a batch of complete messages through the native
|
|
50
|
+
streaming vector API. Both stateful classes are context managers and have
|
|
51
|
+
`reset()` and `close()`.
|
|
52
|
+
|
|
53
|
+
For direct access to `ccobs_create`, `ccobs_encodev`, and the rest of the C API,
|
|
54
|
+
use `ccobs.load_library()`. Its functions have their ctypes `argtypes` and
|
|
55
|
+
`restype` declarations installed.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyccobs"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Portable ctypes bindings for ccobs"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "0BSD"
|
|
12
|
+
license-files = ["LICENSE", "vendor/ccobs/LICENSE"]
|
|
13
|
+
authors = [{ name = "Elizabeth Kiara Regina Ashford" }]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: C",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
test = ["pytest>=7"]
|
|
21
|
+
|
|
22
|
+
[tool.setuptools]
|
|
23
|
+
package-dir = { "" = "src" }
|
|
24
|
+
include-package-data = true
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
where = ["src"]
|
|
28
|
+
|
|
29
|
+
[tool.pytest.ini_options]
|
|
30
|
+
testpaths = ["tests"]
|
|
31
|
+
|
|
32
|
+
[tool.ruff]
|
|
33
|
+
target-version = "py311"
|
|
34
|
+
|
|
35
|
+
[tool.ruff.lint]
|
|
36
|
+
select = ["E4", "E7", "E9", "F", "I"]
|
|
37
|
+
|
|
38
|
+
[tool.cibuildwheel]
|
|
39
|
+
# ctypes wheels are independent of the Python ABI; build once per platform.
|
|
40
|
+
build = ["cp311-manylinux_*", "cp311-macosx_*", "cp311-win_amd64"]
|
|
41
|
+
archs = ["native"]
|
|
42
|
+
before-build = 'python -m pip install "cmake>=3.15"'
|
|
43
|
+
test-requires = ["pytest>=7"]
|
|
44
|
+
test-sources = ["tests"]
|
|
45
|
+
test-command = "python -m pytest tests"
|
|
46
|
+
|
|
47
|
+
[tool.cibuildwheel.linux]
|
|
48
|
+
manylinux-x86_64-image = "manylinux2014"
|
|
49
|
+
manylinux-aarch64-image = "manylinux2014"
|
pyccobs-0.1.0/setup.cfg
ADDED
pyccobs-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Build the bundled ctypes library with its native CMake project."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from setuptools import Extension, setup
|
|
8
|
+
from setuptools.command.bdist_wheel import bdist_wheel
|
|
9
|
+
from setuptools.command.build_ext import build_ext
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CMakeExtension(Extension):
|
|
13
|
+
"""A native library built by CMake rather than setuptools's compiler."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, name: str) -> None:
|
|
16
|
+
super().__init__(name, sources=[])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CMakeBuild(build_ext):
|
|
20
|
+
"""Configure, build, and install ccobs into the staged Python package."""
|
|
21
|
+
|
|
22
|
+
def build_extension(self, ext: Extension) -> None:
|
|
23
|
+
source_dir = Path(__file__).resolve().parent
|
|
24
|
+
build_dir = Path(self.build_temp).resolve() / ext.name
|
|
25
|
+
extension_path = Path(self.get_ext_fullpath(ext.name)).resolve()
|
|
26
|
+
install_root = extension_path.parents[1]
|
|
27
|
+
config = "Debug" if self.debug else "Release"
|
|
28
|
+
|
|
29
|
+
# Don't let stale artifacts from previous builds leak into the wheel.
|
|
30
|
+
extension_path.unlink(missing_ok=True)
|
|
31
|
+
build_dir.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
configure_command = [
|
|
33
|
+
"cmake",
|
|
34
|
+
"-S",
|
|
35
|
+
str(source_dir),
|
|
36
|
+
"-B",
|
|
37
|
+
str(build_dir),
|
|
38
|
+
f"-DCMAKE_BUILD_TYPE={config}",
|
|
39
|
+
]
|
|
40
|
+
generator = os.environ.get("CMAKE_GENERATOR")
|
|
41
|
+
if generator:
|
|
42
|
+
configure_command.extend(["-G", generator])
|
|
43
|
+
if (
|
|
44
|
+
os.name == "nt"
|
|
45
|
+
and (not generator or generator.startswith("Visual Studio"))
|
|
46
|
+
and "CMAKE_GENERATOR_PLATFORM" not in os.environ
|
|
47
|
+
):
|
|
48
|
+
architecture = {
|
|
49
|
+
"win32": "Win32",
|
|
50
|
+
"win-amd64": "x64",
|
|
51
|
+
"win-arm64": "ARM64",
|
|
52
|
+
}.get(self.plat_name)
|
|
53
|
+
if architecture:
|
|
54
|
+
configure_command.extend(["-A", architecture])
|
|
55
|
+
|
|
56
|
+
subprocess.run(configure_command, check=True)
|
|
57
|
+
subprocess.run(
|
|
58
|
+
["cmake", "--build", str(build_dir), "--config", config], check=True
|
|
59
|
+
)
|
|
60
|
+
subprocess.run(
|
|
61
|
+
[
|
|
62
|
+
"cmake",
|
|
63
|
+
"--install",
|
|
64
|
+
str(build_dir),
|
|
65
|
+
"--config",
|
|
66
|
+
config,
|
|
67
|
+
"--prefix",
|
|
68
|
+
str(install_root),
|
|
69
|
+
],
|
|
70
|
+
check=True,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class PlatformWheel(bdist_wheel):
|
|
75
|
+
"""Tag the ctypes wheel as Python-independent but platform-specific."""
|
|
76
|
+
|
|
77
|
+
def get_tag(self) -> tuple[str, str, str]:
|
|
78
|
+
_, _, platform = super().get_tag()
|
|
79
|
+
return "py3", "none", platform
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
setup(
|
|
83
|
+
ext_modules=[CMakeExtension("ccobs._ccobs")],
|
|
84
|
+
cmdclass={"build_ext": CMakeBuild, "bdist_wheel": PlatformWheel},
|
|
85
|
+
)
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""Pythonic ctypes bindings for the ccobs C library."""
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
import os
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from enum import IntEnum
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from ._lib import BytePointer, StatePointer, bind
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Decoder",
|
|
13
|
+
"Encoder",
|
|
14
|
+
"Error",
|
|
15
|
+
"MalformedInputError",
|
|
16
|
+
"CcobsError",
|
|
17
|
+
"decode",
|
|
18
|
+
"encode",
|
|
19
|
+
"encodev",
|
|
20
|
+
"load_library",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Error(IntEnum):
|
|
25
|
+
OK = 0
|
|
26
|
+
PARTIAL = 1
|
|
27
|
+
NEED_OUTPUT = 2
|
|
28
|
+
NULL_ARGUMENT = 3
|
|
29
|
+
MALFORMED_INPUT = 4
|
|
30
|
+
OOM = 5
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CcobsError(Exception):
|
|
34
|
+
"""An error reported by the native ccobs library."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, code: int, message: str):
|
|
37
|
+
self.code = Error(code) if code in Error._value2member_map_ else code
|
|
38
|
+
super().__init__(message)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class MalformedInputError(CcobsError):
|
|
42
|
+
"""The encoded stream contains a malformed COBS frame."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_library: Optional[ctypes.CDLL] = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_library(path: Optional[os.PathLike[str] | str] = None) -> ctypes.CDLL:
|
|
49
|
+
"""Load and return ccobs, optionally from an explicit path.
|
|
50
|
+
|
|
51
|
+
Calling this with a path replaces the process-wide library used by new
|
|
52
|
+
encoders and decoders. Existing objects retain their original library.
|
|
53
|
+
"""
|
|
54
|
+
global _library
|
|
55
|
+
if path is not None or _library is None:
|
|
56
|
+
_library = bind(path)
|
|
57
|
+
return _library
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _message(lib: ctypes.CDLL, code: int) -> str:
|
|
61
|
+
value = lib.ccobs_error_string(code)
|
|
62
|
+
return value.decode("utf-8", "replace") if value else f"ccobs error {code}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _raise_error(lib: ctypes.CDLL, code: int) -> None:
|
|
66
|
+
"""Wrapper to turn ccobs error into idiomatic Python errors."""
|
|
67
|
+
cls = MalformedInputError if code == Error.MALFORMED_INPUT else CcobsError
|
|
68
|
+
raise cls(code, _message(lib, code))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _input_buffer(data: bytes) -> tuple[object | None, BytePointer]:
|
|
72
|
+
if not data:
|
|
73
|
+
return None, BytePointer()
|
|
74
|
+
owner = (ctypes.c_uint8 * len(data)).from_buffer_copy(data)
|
|
75
|
+
return owner, ctypes.cast(owner, BytePointer)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class _State:
|
|
79
|
+
"""Wrapper to internal ccobs state."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, library: Optional[ctypes.CDLL] = None):
|
|
82
|
+
self._lib = library or load_library()
|
|
83
|
+
error = ctypes.c_int(Error.OK)
|
|
84
|
+
self._state: StatePointer = self._lib.ccobs_create(ctypes.byref(error))
|
|
85
|
+
if not self._state:
|
|
86
|
+
_raise_error(self._lib, error.value)
|
|
87
|
+
|
|
88
|
+
def close(self) -> None:
|
|
89
|
+
state = getattr(self, "_state", None)
|
|
90
|
+
if state:
|
|
91
|
+
self._lib.ccobs_destroy(state)
|
|
92
|
+
self._state = StatePointer()
|
|
93
|
+
|
|
94
|
+
def reset(self) -> None:
|
|
95
|
+
code = self._lib.ccobs_reset(self._state)
|
|
96
|
+
if code != Error.OK:
|
|
97
|
+
_raise_error(self._lib, code)
|
|
98
|
+
|
|
99
|
+
def __enter__(self):
|
|
100
|
+
return self
|
|
101
|
+
|
|
102
|
+
def __exit__(self, *args) -> None:
|
|
103
|
+
self.close()
|
|
104
|
+
|
|
105
|
+
def __del__(self):
|
|
106
|
+
self.close()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class Encoder(_State):
|
|
110
|
+
"""Incrementally encode message chunks or batches of complete messages."""
|
|
111
|
+
|
|
112
|
+
_OUTPUT_CHUNK_SIZE = 64 * 1024
|
|
113
|
+
|
|
114
|
+
def feed(self, data: object = b"", *, end_message: bool = False) -> bytes:
|
|
115
|
+
raw = memoryview(data).cast("B").tobytes()
|
|
116
|
+
if end_message:
|
|
117
|
+
return self.feedv((raw,))
|
|
118
|
+
|
|
119
|
+
owner, _ = _input_buffer(raw)
|
|
120
|
+
offset = 0
|
|
121
|
+
result = bytearray()
|
|
122
|
+
# 257 also drains a full native block retained from an earlier call.
|
|
123
|
+
capacity = max(257, int(self._lib.ccobs_encode_max_size(len(raw))))
|
|
124
|
+
while True:
|
|
125
|
+
output = (ctypes.c_uint8 * capacity)()
|
|
126
|
+
available = ctypes.c_size_t(len(raw) - offset)
|
|
127
|
+
size = ctypes.c_size_t(capacity)
|
|
128
|
+
current = (
|
|
129
|
+
ctypes.cast(ctypes.byref(owner, offset), BytePointer)
|
|
130
|
+
if owner
|
|
131
|
+
else BytePointer()
|
|
132
|
+
)
|
|
133
|
+
code = self._lib.ccobs_encode(
|
|
134
|
+
self._state,
|
|
135
|
+
current,
|
|
136
|
+
ctypes.byref(available),
|
|
137
|
+
output,
|
|
138
|
+
ctypes.byref(size),
|
|
139
|
+
end_message,
|
|
140
|
+
)
|
|
141
|
+
offset += available.value
|
|
142
|
+
result.extend(output[: size.value])
|
|
143
|
+
|
|
144
|
+
if code == Error.NEED_OUTPUT:
|
|
145
|
+
continue
|
|
146
|
+
if code not in (Error.OK, Error.PARTIAL):
|
|
147
|
+
_raise_error(self._lib, code)
|
|
148
|
+
|
|
149
|
+
return bytes(result)
|
|
150
|
+
|
|
151
|
+
def feedv(self, messages: Iterable[object]) -> bytes:
|
|
152
|
+
"""Encode complete, independent messages into one flat framed stream.
|
|
153
|
+
|
|
154
|
+
The encoder state is retained between calls. If :meth:`feed` previously
|
|
155
|
+
supplied an unfinished message, the first message completes it.
|
|
156
|
+
"""
|
|
157
|
+
pending = [memoryview(message).cast("B").tobytes() for message in messages]
|
|
158
|
+
if not pending:
|
|
159
|
+
return b""
|
|
160
|
+
|
|
161
|
+
offsets = [0] * len(pending)
|
|
162
|
+
result = bytearray()
|
|
163
|
+
while pending:
|
|
164
|
+
count = len(pending)
|
|
165
|
+
owners: list[object | None] = []
|
|
166
|
+
pointers = (BytePointer * count)()
|
|
167
|
+
sizes = (ctypes.c_size_t * count)()
|
|
168
|
+
maximum = 0
|
|
169
|
+
for index, raw in enumerate(pending):
|
|
170
|
+
owner, pointer = _input_buffer(raw)
|
|
171
|
+
owners.append(owner)
|
|
172
|
+
offset = offsets[index]
|
|
173
|
+
pointers[index] = (
|
|
174
|
+
ctypes.cast(ctypes.byref(owner, offset), BytePointer)
|
|
175
|
+
if owner
|
|
176
|
+
else pointer
|
|
177
|
+
)
|
|
178
|
+
sizes[index] = len(raw) - offset
|
|
179
|
+
maximum += int(self._lib.ccobs_encode_max_size(sizes[index]))
|
|
180
|
+
|
|
181
|
+
capacity = max(257, min(maximum, self._OUTPUT_CHUNK_SIZE))
|
|
182
|
+
output = (ctypes.c_uint8 * capacity)()
|
|
183
|
+
completed = ctypes.c_size_t(count)
|
|
184
|
+
written = ctypes.c_size_t(capacity)
|
|
185
|
+
code = self._lib.ccobs_encodev(
|
|
186
|
+
self._state,
|
|
187
|
+
pointers,
|
|
188
|
+
sizes,
|
|
189
|
+
ctypes.byref(completed),
|
|
190
|
+
output,
|
|
191
|
+
ctypes.byref(written),
|
|
192
|
+
)
|
|
193
|
+
result.extend(output[: written.value])
|
|
194
|
+
|
|
195
|
+
for index in range(min(completed.value + 1, count)):
|
|
196
|
+
offsets[index] += sizes[index]
|
|
197
|
+
if completed.value:
|
|
198
|
+
del pending[: completed.value]
|
|
199
|
+
del offsets[: completed.value]
|
|
200
|
+
|
|
201
|
+
if code == Error.NEED_OUTPUT:
|
|
202
|
+
continue
|
|
203
|
+
if code != Error.OK:
|
|
204
|
+
_raise_error(self._lib, code)
|
|
205
|
+
if completed.value != count:
|
|
206
|
+
raise CcobsError(
|
|
207
|
+
Error.PARTIAL, "ccobs_encodev did not complete the batch"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return bytes(result)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class Decoder(_State):
|
|
214
|
+
"""Incrementally decode framed stream chunks."""
|
|
215
|
+
|
|
216
|
+
def feed(self, data: object = b"") -> tuple[bytes, int, bool]:
|
|
217
|
+
"""Decode at most one frame and report whether it was completed."""
|
|
218
|
+
raw = memoryview(data).cast("B").tobytes()
|
|
219
|
+
owner, _ = _input_buffer(raw)
|
|
220
|
+
offset = 0
|
|
221
|
+
result = bytearray()
|
|
222
|
+
complete = ctypes.c_bool(False)
|
|
223
|
+
capacity = max(1, len(raw))
|
|
224
|
+
while True:
|
|
225
|
+
output = (ctypes.c_uint8 * capacity)()
|
|
226
|
+
available = ctypes.c_size_t(len(raw) - offset)
|
|
227
|
+
size = ctypes.c_size_t(capacity)
|
|
228
|
+
current = (
|
|
229
|
+
ctypes.cast(ctypes.byref(owner, offset), BytePointer)
|
|
230
|
+
if owner
|
|
231
|
+
else BytePointer()
|
|
232
|
+
)
|
|
233
|
+
code = self._lib.ccobs_decode(
|
|
234
|
+
self._state,
|
|
235
|
+
current,
|
|
236
|
+
ctypes.byref(available),
|
|
237
|
+
output,
|
|
238
|
+
ctypes.byref(size),
|
|
239
|
+
ctypes.byref(complete),
|
|
240
|
+
)
|
|
241
|
+
offset += available.value
|
|
242
|
+
result.extend(output[: size.value])
|
|
243
|
+
if code == Error.NEED_OUTPUT:
|
|
244
|
+
continue
|
|
245
|
+
if code not in (Error.OK, Error.PARTIAL):
|
|
246
|
+
_raise_error(self._lib, code)
|
|
247
|
+
|
|
248
|
+
return bytes(result), offset, complete.value
|
|
249
|
+
|
|
250
|
+
def feedv(self, data: object = b"") -> tuple[bytes, int, tuple[int, ...]]:
|
|
251
|
+
"""Decode framed stream data into one flat byte string.
|
|
252
|
+
|
|
253
|
+
The decoder state is retained between calls. The returned tuple contains
|
|
254
|
+
the decoded bytes, consumed input byte count, and completed frame sizes.
|
|
255
|
+
If a frame began in a previous call, its size includes bytes returned by
|
|
256
|
+
that call.
|
|
257
|
+
"""
|
|
258
|
+
raw = memoryview(data).cast("B").tobytes()
|
|
259
|
+
owner, _ = _input_buffer(raw)
|
|
260
|
+
output_capacity = max(1, len(raw))
|
|
261
|
+
output = (ctypes.c_uint8 * output_capacity)()
|
|
262
|
+
available = ctypes.c_size_t(len(raw))
|
|
263
|
+
written = ctypes.c_size_t(output_capacity)
|
|
264
|
+
|
|
265
|
+
# Every complete encoded frame ends in zero. The extra slot lets
|
|
266
|
+
# decodev consume a partial frame following the final delimiter.
|
|
267
|
+
frame_capacity = raw.count(0) + 1
|
|
268
|
+
frame_sizes = (ctypes.c_size_t * frame_capacity)()
|
|
269
|
+
frame_count = ctypes.c_size_t(frame_capacity)
|
|
270
|
+
code = self._lib.ccobs_decodev(
|
|
271
|
+
self._state,
|
|
272
|
+
ctypes.cast(owner, BytePointer) if owner else BytePointer(),
|
|
273
|
+
ctypes.byref(available),
|
|
274
|
+
output,
|
|
275
|
+
ctypes.byref(written),
|
|
276
|
+
frame_sizes,
|
|
277
|
+
ctypes.byref(frame_count),
|
|
278
|
+
)
|
|
279
|
+
if code not in (Error.OK, Error.PARTIAL):
|
|
280
|
+
_raise_error(self._lib, code)
|
|
281
|
+
|
|
282
|
+
sizes = tuple(frame_sizes[: frame_count.value])
|
|
283
|
+
return bytes(output[: written.value]), available.value, sizes
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def encode(data: object, *, library: Optional[ctypes.CDLL] = None) -> bytes:
|
|
287
|
+
"""Encode one bytes-like message, including its trailing zero delimiter."""
|
|
288
|
+
with Encoder(library) as encoder:
|
|
289
|
+
return encoder.feed(data, end_message=True)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def encodev(
|
|
293
|
+
messages: Iterable[object], *, library: Optional[ctypes.CDLL] = None
|
|
294
|
+
) -> bytes:
|
|
295
|
+
"""Encode a batch of bytes-like messages into one flat framed stream."""
|
|
296
|
+
with Encoder(library) as encoder:
|
|
297
|
+
return encoder.feedv(messages)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def decode(data: object, *, library: Optional[ctypes.CDLL] = None) -> bytes:
|
|
301
|
+
"""Decode exactly one complete, zero-delimited frame."""
|
|
302
|
+
raw = memoryview(data).cast("B").tobytes()
|
|
303
|
+
with Decoder(library) as decoder:
|
|
304
|
+
output, consumed, complete = decoder.feed(raw)
|
|
305
|
+
|
|
306
|
+
if not complete:
|
|
307
|
+
raise ValueError("incomplete COBS frame")
|
|
308
|
+
|
|
309
|
+
if consumed != len(raw):
|
|
310
|
+
raise ValueError("input contains data after the first COBS frame")
|
|
311
|
+
|
|
312
|
+
return output
|