crc8-pure 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.
- crc8_pure-0.1.0/LICENSE +21 -0
- crc8_pure-0.1.0/PKG-INFO +152 -0
- crc8_pure-0.1.0/README.md +135 -0
- crc8_pure-0.1.0/crc8_pure/__init__.py +93 -0
- crc8_pure-0.1.0/crc8_pure/__main__.py +6 -0
- crc8_pure-0.1.0/crc8_pure/_class.py +33 -0
- crc8_pure-0.1.0/crc8_pure/_variants.py +70 -0
- crc8_pure-0.1.0/crc8_pure.egg-info/PKG-INFO +152 -0
- crc8_pure-0.1.0/crc8_pure.egg-info/SOURCES.txt +13 -0
- crc8_pure-0.1.0/crc8_pure.egg-info/dependency_links.txt +1 -0
- crc8_pure-0.1.0/crc8_pure.egg-info/entry_points.txt +2 -0
- crc8_pure-0.1.0/crc8_pure.egg-info/top_level.txt +1 -0
- crc8_pure-0.1.0/pyproject.toml +30 -0
- crc8_pure-0.1.0/setup.cfg +4 -0
- crc8_pure-0.1.0/tests/test_crc8_pure.py +302 -0
crc8_pure-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 crc8-pure contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
crc8_pure-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crc8-pure
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Zero-dependency, pure-Python CRC-8 over 7 RevEng-catalogued variants
|
|
5
|
+
Author: crc8-pure contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: crc,crc8,checksum,pure-python,zero-dependency
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# crc8-pure
|
|
19
|
+
|
|
20
|
+
Zero-dependency, pure-Python CRC-8 over 7 RevEng-catalogued variants.
|
|
21
|
+
|
|
22
|
+
`crc8-pure` provides a single, self-contained implementation of seven widely
|
|
23
|
+
used CRC-8 variants — `crc-8`, `crc-8-darc`, `crc-8-i-code`, `crc-8-itu`
|
|
24
|
+
(CCITT), `crc-8-maxim` (DOW), `crc-8-rohc`, and `crc-8-wcdma` — together
|
|
25
|
+
with a class-based incremental API and a command-line entry point. No
|
|
26
|
+
C-extension dependencies; only the Python standard library is used at
|
|
27
|
+
runtime.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install crc8-pure
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or from a local checkout:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install -e .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Requires Python ≥ 3.8. There are no runtime dependencies.
|
|
42
|
+
|
|
43
|
+
## Quickstart
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import crc8_pure
|
|
47
|
+
|
|
48
|
+
# Functional API: returns an int 0–255
|
|
49
|
+
crc8_pure.crc8(b"hello") # crc-8 variant (default)
|
|
50
|
+
crc8_pure.crc8(b"hello", variant="crc-8-maxim")
|
|
51
|
+
crc8_pure.crc8_maxim(b"hello") # convenience wrapper
|
|
52
|
+
|
|
53
|
+
# Incremental / streaming API
|
|
54
|
+
crc8_pure.crc8(b"hel", init=0x00)
|
|
55
|
+
crc8_pure.crc8(b"lo", init=<prev>) # continues from previous state
|
|
56
|
+
|
|
57
|
+
# Class API
|
|
58
|
+
h = crc8_pure.CRC8(variant="crc-8-maxim")
|
|
59
|
+
h.update(b"hello")
|
|
60
|
+
h.digest() # -> bytes
|
|
61
|
+
h.hexdigest() # -> "3b"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### CLI
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# String input
|
|
68
|
+
python -m crc8_pure crc8-maxim "123456789"
|
|
69
|
+
|
|
70
|
+
# Hex-prefixed output
|
|
71
|
+
python -m crc8_pure crc8 "123456789" --hex
|
|
72
|
+
|
|
73
|
+
# File input
|
|
74
|
+
python -m crc8_pure crc8-maxim path/to/file.bin
|
|
75
|
+
|
|
76
|
+
# Read from stdin
|
|
77
|
+
cat file.bin | python -m crc8_pure crc-8 -
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## 7 Variants
|
|
81
|
+
|
|
82
|
+
| Variant | Polynomial | Init | RefIn | RefOut | XOR Out | Check (`"123456789"`) |
|
|
83
|
+
|----------------|-----------:|-----:|:-----:|:------:|--------:|---------------------:|
|
|
84
|
+
| `crc-8` | `0x07` | `0x00` | no | no | `0x00` | `0xF4` |
|
|
85
|
+
| `crc-8-darc` | `0x39` | `0x00` | yes | yes | `0x00` | `0x15` |
|
|
86
|
+
| `crc-8-i-code` | `0x1D` | `0xFD` | no | no | `0x00` | `0x7E` |
|
|
87
|
+
| `crc-8-itu` | `0x07` | `0x55` | no | no | `0x55` | `0xA1` |
|
|
88
|
+
| `crc-8-maxim` | `0x31` | `0x00` | yes | yes | `0x00` | `0xA1` |
|
|
89
|
+
| `crc-8-rohc` | `0x07` | `0xFF` | yes | yes | `0x00` | `0xD0` |
|
|
90
|
+
| `crc-8-wcdma` | `0x9B` | `0x00` | yes | yes | `0x00` | `0x25` |
|
|
91
|
+
|
|
92
|
+
Check values come from the
|
|
93
|
+
[RevEng CRC Catalogue](https://reveng.sourceforge.io/crc-catalogue/all.htm)
|
|
94
|
+
and have been verified byte-exact against `crcmod`.
|
|
95
|
+
|
|
96
|
+
Aliases: `CCITT` → `crc-8-itu`, `MAXIM` → `crc-8-maxim`,
|
|
97
|
+
`DOW-CRC` → `crc-8-maxim`.
|
|
98
|
+
|
|
99
|
+
## API
|
|
100
|
+
|
|
101
|
+
### `crc8(data=b"", variant="crc-8", init=None) -> int`
|
|
102
|
+
|
|
103
|
+
Compute a CRC-8 checksum. `init` lets you resume from a previous partial
|
|
104
|
+
result: `crc8(b"lo", init=crc8(b"hel"))` is equivalent to
|
|
105
|
+
`crc8(b"hello")`.
|
|
106
|
+
|
|
107
|
+
### `CRC8(variant="crc-8", init=None)`
|
|
108
|
+
|
|
109
|
+
Incremental calculator. Use `update(data)` to feed bytes, then
|
|
110
|
+
`digest()` (bytes) or `hexdigest()` (string).
|
|
111
|
+
|
|
112
|
+
### CLI
|
|
113
|
+
|
|
114
|
+
`python -m crc8_pure <variant> [FILE_OR_STRING...] [--hex]`
|
|
115
|
+
|
|
116
|
+
If no input is given (or `-`), data is read from stdin. A single argument
|
|
117
|
+
is treated as a file path if it exists, otherwise as a string literal.
|
|
118
|
+
Multiple arguments are concatenated.
|
|
119
|
+
|
|
120
|
+
## Tests
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
pip install pytest
|
|
124
|
+
pytest -q
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The test suite covers all 21 canonical vectors (3 inputs × 7 variants)
|
|
128
|
+
plus boundary conditions, alias resolution, incremental equivalence,
|
|
129
|
+
class API, and CLI smoke tests.
|
|
130
|
+
|
|
131
|
+
Current test count: **126 tests**.
|
|
132
|
+
|
|
133
|
+
## Known Issues / Limitations
|
|
134
|
+
|
|
135
|
+
The following are honest documented limits of crc8-pure:
|
|
136
|
+
|
|
137
|
+
- **CRC-8 is not cryptographic.** CRC-8 is for error-detection on trusted media; do not
|
|
138
|
+
use it as a cryptographic integrity check or MAC.
|
|
139
|
+
- **CLI stdin/file materialization.** `python -m crc8_pure <variant> <file>` fully
|
|
140
|
+
materializes the file into RAM before processing. For multi-GB inputs this may
|
|
141
|
+
exhaust memory; no streaming boundary is applied.
|
|
142
|
+
- **Thread-safety.** A `CRC8` instance is not safe for concurrent `update()` calls
|
|
143
|
+
from multiple threads; create one instance per thread.
|
|
144
|
+
- **`data` accepts non-bytes silently.** Pass bytes-like objects (bytes/bytearray/
|
|
145
|
+
memoryview). Passing a non-bytes value will produce a non-canonical result
|
|
146
|
+
without raising an error.
|
|
147
|
+
- **No `CRC8.copy()` / `CRC8.reset()`.** Re-using a `CRC8` instance requires
|
|
148
|
+
constructing a new one; there is no `copy()` or `reset()` method.
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT — see `LICENSE`.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# crc8-pure
|
|
2
|
+
|
|
3
|
+
Zero-dependency, pure-Python CRC-8 over 7 RevEng-catalogued variants.
|
|
4
|
+
|
|
5
|
+
`crc8-pure` provides a single, self-contained implementation of seven widely
|
|
6
|
+
used CRC-8 variants — `crc-8`, `crc-8-darc`, `crc-8-i-code`, `crc-8-itu`
|
|
7
|
+
(CCITT), `crc-8-maxim` (DOW), `crc-8-rohc`, and `crc-8-wcdma` — together
|
|
8
|
+
with a class-based incremental API and a command-line entry point. No
|
|
9
|
+
C-extension dependencies; only the Python standard library is used at
|
|
10
|
+
runtime.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install crc8-pure
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or from a local checkout:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install -e .
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Requires Python ≥ 3.8. There are no runtime dependencies.
|
|
25
|
+
|
|
26
|
+
## Quickstart
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import crc8_pure
|
|
30
|
+
|
|
31
|
+
# Functional API: returns an int 0–255
|
|
32
|
+
crc8_pure.crc8(b"hello") # crc-8 variant (default)
|
|
33
|
+
crc8_pure.crc8(b"hello", variant="crc-8-maxim")
|
|
34
|
+
crc8_pure.crc8_maxim(b"hello") # convenience wrapper
|
|
35
|
+
|
|
36
|
+
# Incremental / streaming API
|
|
37
|
+
crc8_pure.crc8(b"hel", init=0x00)
|
|
38
|
+
crc8_pure.crc8(b"lo", init=<prev>) # continues from previous state
|
|
39
|
+
|
|
40
|
+
# Class API
|
|
41
|
+
h = crc8_pure.CRC8(variant="crc-8-maxim")
|
|
42
|
+
h.update(b"hello")
|
|
43
|
+
h.digest() # -> bytes
|
|
44
|
+
h.hexdigest() # -> "3b"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### CLI
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# String input
|
|
51
|
+
python -m crc8_pure crc8-maxim "123456789"
|
|
52
|
+
|
|
53
|
+
# Hex-prefixed output
|
|
54
|
+
python -m crc8_pure crc8 "123456789" --hex
|
|
55
|
+
|
|
56
|
+
# File input
|
|
57
|
+
python -m crc8_pure crc8-maxim path/to/file.bin
|
|
58
|
+
|
|
59
|
+
# Read from stdin
|
|
60
|
+
cat file.bin | python -m crc8_pure crc-8 -
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 7 Variants
|
|
64
|
+
|
|
65
|
+
| Variant | Polynomial | Init | RefIn | RefOut | XOR Out | Check (`"123456789"`) |
|
|
66
|
+
|----------------|-----------:|-----:|:-----:|:------:|--------:|---------------------:|
|
|
67
|
+
| `crc-8` | `0x07` | `0x00` | no | no | `0x00` | `0xF4` |
|
|
68
|
+
| `crc-8-darc` | `0x39` | `0x00` | yes | yes | `0x00` | `0x15` |
|
|
69
|
+
| `crc-8-i-code` | `0x1D` | `0xFD` | no | no | `0x00` | `0x7E` |
|
|
70
|
+
| `crc-8-itu` | `0x07` | `0x55` | no | no | `0x55` | `0xA1` |
|
|
71
|
+
| `crc-8-maxim` | `0x31` | `0x00` | yes | yes | `0x00` | `0xA1` |
|
|
72
|
+
| `crc-8-rohc` | `0x07` | `0xFF` | yes | yes | `0x00` | `0xD0` |
|
|
73
|
+
| `crc-8-wcdma` | `0x9B` | `0x00` | yes | yes | `0x00` | `0x25` |
|
|
74
|
+
|
|
75
|
+
Check values come from the
|
|
76
|
+
[RevEng CRC Catalogue](https://reveng.sourceforge.io/crc-catalogue/all.htm)
|
|
77
|
+
and have been verified byte-exact against `crcmod`.
|
|
78
|
+
|
|
79
|
+
Aliases: `CCITT` → `crc-8-itu`, `MAXIM` → `crc-8-maxim`,
|
|
80
|
+
`DOW-CRC` → `crc-8-maxim`.
|
|
81
|
+
|
|
82
|
+
## API
|
|
83
|
+
|
|
84
|
+
### `crc8(data=b"", variant="crc-8", init=None) -> int`
|
|
85
|
+
|
|
86
|
+
Compute a CRC-8 checksum. `init` lets you resume from a previous partial
|
|
87
|
+
result: `crc8(b"lo", init=crc8(b"hel"))` is equivalent to
|
|
88
|
+
`crc8(b"hello")`.
|
|
89
|
+
|
|
90
|
+
### `CRC8(variant="crc-8", init=None)`
|
|
91
|
+
|
|
92
|
+
Incremental calculator. Use `update(data)` to feed bytes, then
|
|
93
|
+
`digest()` (bytes) or `hexdigest()` (string).
|
|
94
|
+
|
|
95
|
+
### CLI
|
|
96
|
+
|
|
97
|
+
`python -m crc8_pure <variant> [FILE_OR_STRING...] [--hex]`
|
|
98
|
+
|
|
99
|
+
If no input is given (or `-`), data is read from stdin. A single argument
|
|
100
|
+
is treated as a file path if it exists, otherwise as a string literal.
|
|
101
|
+
Multiple arguments are concatenated.
|
|
102
|
+
|
|
103
|
+
## Tests
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
pip install pytest
|
|
107
|
+
pytest -q
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The test suite covers all 21 canonical vectors (3 inputs × 7 variants)
|
|
111
|
+
plus boundary conditions, alias resolution, incremental equivalence,
|
|
112
|
+
class API, and CLI smoke tests.
|
|
113
|
+
|
|
114
|
+
Current test count: **126 tests**.
|
|
115
|
+
|
|
116
|
+
## Known Issues / Limitations
|
|
117
|
+
|
|
118
|
+
The following are honest documented limits of crc8-pure:
|
|
119
|
+
|
|
120
|
+
- **CRC-8 is not cryptographic.** CRC-8 is for error-detection on trusted media; do not
|
|
121
|
+
use it as a cryptographic integrity check or MAC.
|
|
122
|
+
- **CLI stdin/file materialization.** `python -m crc8_pure <variant> <file>` fully
|
|
123
|
+
materializes the file into RAM before processing. For multi-GB inputs this may
|
|
124
|
+
exhaust memory; no streaming boundary is applied.
|
|
125
|
+
- **Thread-safety.** A `CRC8` instance is not safe for concurrent `update()` calls
|
|
126
|
+
from multiple threads; create one instance per thread.
|
|
127
|
+
- **`data` accepts non-bytes silently.** Pass bytes-like objects (bytes/bytearray/
|
|
128
|
+
memoryview). Passing a non-bytes value will produce a non-canonical result
|
|
129
|
+
without raising an error.
|
|
130
|
+
- **No `CRC8.copy()` / `CRC8.reset()`.** Re-using a `CRC8` instance requires
|
|
131
|
+
constructing a new one; there is no `copy()` or `reset()` method.
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT — see `LICENSE`.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""crc8-pure: zero-dependency CRC-8 over 7 RevEng-catalogued variants."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from ._variants import VARIANTS, normalize_variant
|
|
7
|
+
from ._class import CRC8
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _compute(data, variant: str, init):
|
|
13
|
+
name = normalize_variant(variant)
|
|
14
|
+
poly, raw_init, refin, refout, xorout, _check = VARIANTS[name]
|
|
15
|
+
from ._variants import TABLES # local import keeps __init__ slim
|
|
16
|
+
table = TABLES[name]
|
|
17
|
+
if init is None:
|
|
18
|
+
crc = (raw_init ^ xorout) & 0xFF
|
|
19
|
+
else:
|
|
20
|
+
crc = (init ^ xorout) & 0xFF
|
|
21
|
+
for b in data:
|
|
22
|
+
crc = table[(crc ^ b) & 0xFF]
|
|
23
|
+
if refout and not refin:
|
|
24
|
+
from ._variants import reflect8
|
|
25
|
+
crc = reflect8(crc)
|
|
26
|
+
return (crc ^ xorout) & 0xFF
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def crc8(data=b"", variant: str = "crc-8", init=None) -> int:
|
|
30
|
+
"""Compute CRC-8 checksum; returns int 0-255."""
|
|
31
|
+
return _compute(data, variant, init)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def crc8_darc(data) -> int: return crc8(data, "crc-8-darc")
|
|
35
|
+
def crc8_i_code(data) -> int: return crc8(data, "crc-8-i-code")
|
|
36
|
+
def crc8_itu(data) -> int: return crc8(data, "crc-8-itu")
|
|
37
|
+
def crc8_maxim(data) -> int: return crc8(data, "crc-8-maxim")
|
|
38
|
+
def crc8_rohc(data) -> int: return crc8(data, "crc-8-rohc")
|
|
39
|
+
def crc8_wcdma(data) -> int: return crc8(data, "crc-8-wcdma")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"__version__", "CRC8", "crc8",
|
|
44
|
+
"crc8_darc", "crc8_i_code", "crc8_itu",
|
|
45
|
+
"crc8_maxim", "crc8_rohc", "crc8_wcdma",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_VARIANT_NAMES = sorted(VARIANTS.keys())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _isfile(path: str) -> bool:
|
|
53
|
+
try:
|
|
54
|
+
with open(path, "rb"):
|
|
55
|
+
return True
|
|
56
|
+
except OSError:
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _read_file(path: str) -> bytes:
|
|
61
|
+
with open(path, "rb") as f:
|
|
62
|
+
return f.read()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _main(argv):
|
|
66
|
+
p = argparse.ArgumentParser(prog="crc8_pure", description="CRC-8 checksum CLI")
|
|
67
|
+
p.add_argument("variant", help=f"variant name: {', '.join(_VARIANT_NAMES)} (or alias)")
|
|
68
|
+
p.add_argument("inputs", nargs="*", help="files or string literals (stdin if none)")
|
|
69
|
+
p.add_argument("--hex", action="store_true", help="print as 0xNN")
|
|
70
|
+
args = p.parse_args(argv)
|
|
71
|
+
if not args.inputs or args.inputs == ["-"]:
|
|
72
|
+
data = sys.stdin.buffer.read()
|
|
73
|
+
elif len(args.inputs) == 1 and _isfile(args.inputs[0]):
|
|
74
|
+
data = _read_file(args.inputs[0])
|
|
75
|
+
elif len(args.inputs) == 1:
|
|
76
|
+
data = args.inputs[0].encode("utf-8")
|
|
77
|
+
else:
|
|
78
|
+
parts = [_read_file(x) if _isfile(x) else x.encode("utf-8") for x in args.inputs]
|
|
79
|
+
data = b"".join(parts)
|
|
80
|
+
val = crc8(data, args.variant)
|
|
81
|
+
if args.hex:
|
|
82
|
+
print(f"0x{val:02x}")
|
|
83
|
+
else:
|
|
84
|
+
print(f"{val:02x}")
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
sys.exit(_main(sys.argv[1:]))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _cli_entry():
|
|
93
|
+
sys.exit(_main(sys.argv[1:]))
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""CRC8 incremental class for crc8-pure."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from ._variants import VARIANTS, TABLES, normalize_variant, reflect8
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CRC8:
|
|
8
|
+
"""Incremental CRC-8 calculator. ``update(data)`` returns self for chaining."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, variant: str = "crc-8", init=None):
|
|
11
|
+
self.variant = normalize_variant(variant)
|
|
12
|
+
_p, raw_init, self._refin, self._refout, self._xorout, _ = VARIANTS[self.variant]
|
|
13
|
+
self._table = TABLES[self.variant]
|
|
14
|
+
if init is None:
|
|
15
|
+
# Default initial register = raw_init XOR xorout (crcmod convention).
|
|
16
|
+
self._crc = (raw_init ^ self._xorout) & 0xFF
|
|
17
|
+
else:
|
|
18
|
+
# Resuming: ``init`` is the post-xorout digest of a previous step.
|
|
19
|
+
self._crc = (init ^ self._xorout) & 0xFF
|
|
20
|
+
|
|
21
|
+
def update(self, data) -> "CRC8":
|
|
22
|
+
for b in data:
|
|
23
|
+
self._crc = self._table[(self._crc ^ b) & 0xFF]
|
|
24
|
+
return self
|
|
25
|
+
|
|
26
|
+
def digest(self) -> bytes:
|
|
27
|
+
crc = self._crc
|
|
28
|
+
if self._refout and not self._refin:
|
|
29
|
+
crc = reflect8(crc)
|
|
30
|
+
return bytes([(crc ^ self._xorout) & 0xFF])
|
|
31
|
+
|
|
32
|
+
def hexdigest(self) -> str:
|
|
33
|
+
return f"{self.digest()[0]:02x}"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Variant table + reflected-bit helper for crc8-pure.
|
|
2
|
+
|
|
3
|
+
Defines the 7 RevEng-catalogued CRC-8 variants as a frozen dict of
|
|
4
|
+
``(poly, init, refin, refout, xorout, check_on_123456789)`` tuples, plus
|
|
5
|
+
alias table and the ``_reflect8`` bit-reversal helper. Imported by
|
|
6
|
+
``crc8_pure.__init__`` and by ``crc8_pure._class`` (the CRC8 class).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
# (poly, init, refin, refout, xorout, check_on_123456789)
|
|
11
|
+
VARIANTS = {
|
|
12
|
+
"crc-8": (0x07, 0x00, False, False, 0x00, 0xF4),
|
|
13
|
+
"crc-8-darc": (0x39, 0x00, True, True, 0x00, 0x15),
|
|
14
|
+
"crc-8-i-code": (0x1D, 0xFD, False, False, 0x00, 0x7E),
|
|
15
|
+
"crc-8-itu": (0x07, 0x55, False, False, 0x55, 0xA1),
|
|
16
|
+
"crc-8-maxim": (0x31, 0x00, True, True, 0x00, 0xA1),
|
|
17
|
+
"crc-8-rohc": (0x07, 0xFF, True, True, 0x00, 0xD0),
|
|
18
|
+
"crc-8-wcdma": (0x9B, 0x00, True, True, 0x00, 0x25),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
ALIASES = {
|
|
22
|
+
"CCITT": "crc-8-itu",
|
|
23
|
+
"MAXIM": "crc-8-maxim",
|
|
24
|
+
"DOW-CRC": "crc-8-maxim",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def normalize_variant(name: str) -> str:
|
|
29
|
+
key = name.strip().upper().replace("_", "-")
|
|
30
|
+
if key in ALIASES:
|
|
31
|
+
return ALIASES[key]
|
|
32
|
+
for v in VARIANTS:
|
|
33
|
+
if v.upper() == key:
|
|
34
|
+
return v
|
|
35
|
+
raise ValueError(f"unknown crc8 variant: {name!r}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def reflect8(x: int) -> int:
|
|
39
|
+
r = 0
|
|
40
|
+
for i in range(8):
|
|
41
|
+
if x & (1 << i):
|
|
42
|
+
r |= 1 << (7 - i)
|
|
43
|
+
return r & 0xFF
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def build_table(poly: int, refin: bool) -> tuple:
|
|
47
|
+
tbl = []
|
|
48
|
+
if refin:
|
|
49
|
+
rpoly = reflect8(poly)
|
|
50
|
+
for i in range(256):
|
|
51
|
+
crc = i
|
|
52
|
+
for _ in range(8):
|
|
53
|
+
crc = ((crc >> 1) ^ rpoly) if (crc & 1) else (crc >> 1)
|
|
54
|
+
crc &= 0xFF
|
|
55
|
+
tbl.append(crc)
|
|
56
|
+
else:
|
|
57
|
+
for i in range(256):
|
|
58
|
+
crc = i << 8
|
|
59
|
+
for _ in range(8):
|
|
60
|
+
if crc & 0x8000:
|
|
61
|
+
crc = ((crc << 1) ^ (poly << 8)) & 0xFFFF
|
|
62
|
+
else:
|
|
63
|
+
crc = (crc << 1) & 0xFFFF
|
|
64
|
+
tbl.append((crc >> 8) & 0xFF)
|
|
65
|
+
return tuple(tbl)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
TABLES = {
|
|
69
|
+
name: build_table(p, ri) for name, (p, _, ri, _, _, _) in VARIANTS.items()
|
|
70
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crc8-pure
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Zero-dependency, pure-Python CRC-8 over 7 RevEng-catalogued variants
|
|
5
|
+
Author: crc8-pure contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: crc,crc8,checksum,pure-python,zero-dependency
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# crc8-pure
|
|
19
|
+
|
|
20
|
+
Zero-dependency, pure-Python CRC-8 over 7 RevEng-catalogued variants.
|
|
21
|
+
|
|
22
|
+
`crc8-pure` provides a single, self-contained implementation of seven widely
|
|
23
|
+
used CRC-8 variants — `crc-8`, `crc-8-darc`, `crc-8-i-code`, `crc-8-itu`
|
|
24
|
+
(CCITT), `crc-8-maxim` (DOW), `crc-8-rohc`, and `crc-8-wcdma` — together
|
|
25
|
+
with a class-based incremental API and a command-line entry point. No
|
|
26
|
+
C-extension dependencies; only the Python standard library is used at
|
|
27
|
+
runtime.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install crc8-pure
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or from a local checkout:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install -e .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Requires Python ≥ 3.8. There are no runtime dependencies.
|
|
42
|
+
|
|
43
|
+
## Quickstart
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import crc8_pure
|
|
47
|
+
|
|
48
|
+
# Functional API: returns an int 0–255
|
|
49
|
+
crc8_pure.crc8(b"hello") # crc-8 variant (default)
|
|
50
|
+
crc8_pure.crc8(b"hello", variant="crc-8-maxim")
|
|
51
|
+
crc8_pure.crc8_maxim(b"hello") # convenience wrapper
|
|
52
|
+
|
|
53
|
+
# Incremental / streaming API
|
|
54
|
+
crc8_pure.crc8(b"hel", init=0x00)
|
|
55
|
+
crc8_pure.crc8(b"lo", init=<prev>) # continues from previous state
|
|
56
|
+
|
|
57
|
+
# Class API
|
|
58
|
+
h = crc8_pure.CRC8(variant="crc-8-maxim")
|
|
59
|
+
h.update(b"hello")
|
|
60
|
+
h.digest() # -> bytes
|
|
61
|
+
h.hexdigest() # -> "3b"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### CLI
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# String input
|
|
68
|
+
python -m crc8_pure crc8-maxim "123456789"
|
|
69
|
+
|
|
70
|
+
# Hex-prefixed output
|
|
71
|
+
python -m crc8_pure crc8 "123456789" --hex
|
|
72
|
+
|
|
73
|
+
# File input
|
|
74
|
+
python -m crc8_pure crc8-maxim path/to/file.bin
|
|
75
|
+
|
|
76
|
+
# Read from stdin
|
|
77
|
+
cat file.bin | python -m crc8_pure crc-8 -
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## 7 Variants
|
|
81
|
+
|
|
82
|
+
| Variant | Polynomial | Init | RefIn | RefOut | XOR Out | Check (`"123456789"`) |
|
|
83
|
+
|----------------|-----------:|-----:|:-----:|:------:|--------:|---------------------:|
|
|
84
|
+
| `crc-8` | `0x07` | `0x00` | no | no | `0x00` | `0xF4` |
|
|
85
|
+
| `crc-8-darc` | `0x39` | `0x00` | yes | yes | `0x00` | `0x15` |
|
|
86
|
+
| `crc-8-i-code` | `0x1D` | `0xFD` | no | no | `0x00` | `0x7E` |
|
|
87
|
+
| `crc-8-itu` | `0x07` | `0x55` | no | no | `0x55` | `0xA1` |
|
|
88
|
+
| `crc-8-maxim` | `0x31` | `0x00` | yes | yes | `0x00` | `0xA1` |
|
|
89
|
+
| `crc-8-rohc` | `0x07` | `0xFF` | yes | yes | `0x00` | `0xD0` |
|
|
90
|
+
| `crc-8-wcdma` | `0x9B` | `0x00` | yes | yes | `0x00` | `0x25` |
|
|
91
|
+
|
|
92
|
+
Check values come from the
|
|
93
|
+
[RevEng CRC Catalogue](https://reveng.sourceforge.io/crc-catalogue/all.htm)
|
|
94
|
+
and have been verified byte-exact against `crcmod`.
|
|
95
|
+
|
|
96
|
+
Aliases: `CCITT` → `crc-8-itu`, `MAXIM` → `crc-8-maxim`,
|
|
97
|
+
`DOW-CRC` → `crc-8-maxim`.
|
|
98
|
+
|
|
99
|
+
## API
|
|
100
|
+
|
|
101
|
+
### `crc8(data=b"", variant="crc-8", init=None) -> int`
|
|
102
|
+
|
|
103
|
+
Compute a CRC-8 checksum. `init` lets you resume from a previous partial
|
|
104
|
+
result: `crc8(b"lo", init=crc8(b"hel"))` is equivalent to
|
|
105
|
+
`crc8(b"hello")`.
|
|
106
|
+
|
|
107
|
+
### `CRC8(variant="crc-8", init=None)`
|
|
108
|
+
|
|
109
|
+
Incremental calculator. Use `update(data)` to feed bytes, then
|
|
110
|
+
`digest()` (bytes) or `hexdigest()` (string).
|
|
111
|
+
|
|
112
|
+
### CLI
|
|
113
|
+
|
|
114
|
+
`python -m crc8_pure <variant> [FILE_OR_STRING...] [--hex]`
|
|
115
|
+
|
|
116
|
+
If no input is given (or `-`), data is read from stdin. A single argument
|
|
117
|
+
is treated as a file path if it exists, otherwise as a string literal.
|
|
118
|
+
Multiple arguments are concatenated.
|
|
119
|
+
|
|
120
|
+
## Tests
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
pip install pytest
|
|
124
|
+
pytest -q
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The test suite covers all 21 canonical vectors (3 inputs × 7 variants)
|
|
128
|
+
plus boundary conditions, alias resolution, incremental equivalence,
|
|
129
|
+
class API, and CLI smoke tests.
|
|
130
|
+
|
|
131
|
+
Current test count: **126 tests**.
|
|
132
|
+
|
|
133
|
+
## Known Issues / Limitations
|
|
134
|
+
|
|
135
|
+
The following are honest documented limits of crc8-pure:
|
|
136
|
+
|
|
137
|
+
- **CRC-8 is not cryptographic.** CRC-8 is for error-detection on trusted media; do not
|
|
138
|
+
use it as a cryptographic integrity check or MAC.
|
|
139
|
+
- **CLI stdin/file materialization.** `python -m crc8_pure <variant> <file>` fully
|
|
140
|
+
materializes the file into RAM before processing. For multi-GB inputs this may
|
|
141
|
+
exhaust memory; no streaming boundary is applied.
|
|
142
|
+
- **Thread-safety.** A `CRC8` instance is not safe for concurrent `update()` calls
|
|
143
|
+
from multiple threads; create one instance per thread.
|
|
144
|
+
- **`data` accepts non-bytes silently.** Pass bytes-like objects (bytes/bytearray/
|
|
145
|
+
memoryview). Passing a non-bytes value will produce a non-canonical result
|
|
146
|
+
without raising an error.
|
|
147
|
+
- **No `CRC8.copy()` / `CRC8.reset()`.** Re-using a `CRC8` instance requires
|
|
148
|
+
constructing a new one; there is no `copy()` or `reset()` method.
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT — see `LICENSE`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
crc8_pure/__init__.py
|
|
5
|
+
crc8_pure/__main__.py
|
|
6
|
+
crc8_pure/_class.py
|
|
7
|
+
crc8_pure/_variants.py
|
|
8
|
+
crc8_pure.egg-info/PKG-INFO
|
|
9
|
+
crc8_pure.egg-info/SOURCES.txt
|
|
10
|
+
crc8_pure.egg-info/dependency_links.txt
|
|
11
|
+
crc8_pure.egg-info/entry_points.txt
|
|
12
|
+
crc8_pure.egg-info/top_level.txt
|
|
13
|
+
tests/test_crc8_pure.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
crc8_pure
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "crc8-pure"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Zero-dependency, pure-Python CRC-8 over 7 RevEng-catalogued variants"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
authors = [{name = "crc8-pure contributors"}]
|
|
13
|
+
keywords = ["crc", "crc8", "checksum", "pure-python", "zero-dependency"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
20
|
+
]
|
|
21
|
+
dependencies = []
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
crc8-pure = "crc8_pure:_cli_entry"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools]
|
|
27
|
+
packages = ["crc8_pure"]
|
|
28
|
+
|
|
29
|
+
[tool.pytest.ini_options]
|
|
30
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""Tests for crc8_pure — covers all 21 canonical vectors + boundary cases.
|
|
2
|
+
|
|
3
|
+
Test count target: ≥100 (achieved via parametrize across 7 variants x inputs).
|
|
4
|
+
The 21-vector canonical table comes from the RevEng CRC Catalogue and was
|
|
5
|
+
verified at orchestrator time via crcmod.predefined.mkPredefinedCrcFun.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
import crc8_pure as c
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# canonical (variant, input, expected) triples from spec.md table
|
|
19
|
+
VARIANTS = [
|
|
20
|
+
"crc-8", "crc-8-darc", "crc-8-i-code",
|
|
21
|
+
"crc-8-itu", "crc-8-maxim", "crc-8-rohc", "crc-8-wcdma",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
CANONICAL = [
|
|
25
|
+
# (variant, input_bytes, expected_hex)
|
|
26
|
+
("crc-8", b"", 0x00),
|
|
27
|
+
("crc-8", b"a", 0x20),
|
|
28
|
+
("crc-8", b"123456789", 0xF4),
|
|
29
|
+
("crc-8-darc", b"", 0x00),
|
|
30
|
+
("crc-8-darc", b"a", 0x1B),
|
|
31
|
+
("crc-8-darc", b"123456789", 0x15),
|
|
32
|
+
("crc-8-i-code", b"", 0xFD),
|
|
33
|
+
("crc-8-i-code", b"a", 0x77),
|
|
34
|
+
("crc-8-i-code", b"123456789", 0x7E),
|
|
35
|
+
("crc-8-itu", b"", 0x55),
|
|
36
|
+
("crc-8-itu", b"a", 0x75),
|
|
37
|
+
("crc-8-itu", b"123456789", 0xA1),
|
|
38
|
+
("crc-8-maxim", b"", 0x00),
|
|
39
|
+
("crc-8-maxim", b"a", 0x3B),
|
|
40
|
+
("crc-8-maxim", b"123456789", 0xA1),
|
|
41
|
+
("crc-8-rohc", b"", 0xFF),
|
|
42
|
+
("crc-8-rohc", b"a", 0x16),
|
|
43
|
+
("crc-8-rohc", b"123456789", 0xD0),
|
|
44
|
+
("crc-8-wcdma", b"", 0x00),
|
|
45
|
+
("crc-8-wcdma", b"a", 0xE6),
|
|
46
|
+
("crc-8-wcdma", b"123456789", 0x25),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ───────────── 21 canonical vectors ─────────────
|
|
51
|
+
|
|
52
|
+
@pytest.mark.parametrize("variant,data,expected", CANONICAL,
|
|
53
|
+
ids=[f"{v}|{d or 'empty'}" for v, d, _ in CANONICAL])
|
|
54
|
+
def test_canonical_vector(variant, data, expected):
|
|
55
|
+
assert c.crc8(data, variant) == expected
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ───────────── Per-variant convenience functions ─────────────
|
|
59
|
+
|
|
60
|
+
@pytest.mark.parametrize("variant,fn_name,expected", [
|
|
61
|
+
("crc-8-darc", "crc8_darc", 0x1B),
|
|
62
|
+
("crc-8-i-code", "crc8_i_code", 0x77),
|
|
63
|
+
("crc-8-itu", "crc8_itu", 0x75),
|
|
64
|
+
("crc-8-maxim", "crc8_maxim", 0x3B),
|
|
65
|
+
("crc-8-rohc", "crc8_rohc", 0x16),
|
|
66
|
+
("crc-8-wcdma", "crc8_wcdma", 0xE6),
|
|
67
|
+
])
|
|
68
|
+
def test_per_variant_function_a(variant, fn_name, expected):
|
|
69
|
+
assert getattr(c, fn_name)(b"a") == expected
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@pytest.mark.parametrize("variant,fn_name", [
|
|
73
|
+
("crc-8-darc", "crc8_darc"),
|
|
74
|
+
("crc-8-i-code", "crc8_i_code"),
|
|
75
|
+
("crc-8-itu", "crc8_itu"),
|
|
76
|
+
("crc-8-maxim", "crc8_maxim"),
|
|
77
|
+
("crc-8-rohc", "crc8_rohc"),
|
|
78
|
+
("crc-8-wcdma", "crc8_wcdma"),
|
|
79
|
+
])
|
|
80
|
+
def test_per_variant_canonical_123456789(variant, fn_name):
|
|
81
|
+
expected = c.crc8(b"123456789", variant)
|
|
82
|
+
assert getattr(c, fn_name)(b"123456789") == expected
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_per_variant_empty_maxim():
|
|
86
|
+
assert c.crc8_maxim(b"") == 0x00
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_per_variant_empty_rohc():
|
|
90
|
+
assert c.crc8_rohc(b"") == 0xFF
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ───────────── Alias table ─────────────
|
|
94
|
+
|
|
95
|
+
@pytest.mark.parametrize("alias,canonical", [
|
|
96
|
+
("CCITT", "crc-8-itu"),
|
|
97
|
+
("MAXIM", "crc-8-maxim"),
|
|
98
|
+
("DOW-CRC", "crc-8-maxim"),
|
|
99
|
+
])
|
|
100
|
+
def test_alias_equivalence(alias, canonical):
|
|
101
|
+
assert c.crc8(b"hello", alias) == c.crc8(b"hello", canonical)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_case_insensitive_variant_name():
|
|
105
|
+
assert c.crc8(b"a", "CRC-8") == c.crc8(b"a", "crc-8")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_underscore_variant_name():
|
|
109
|
+
assert c.crc8(b"a", "crc_8") == c.crc8(b"a", "crc-8")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_unknown_variant_raises():
|
|
113
|
+
with pytest.raises(ValueError):
|
|
114
|
+
c.crc8(b"a", "crc-99-unknown")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ───────────── Incremental update ─────────────
|
|
118
|
+
|
|
119
|
+
@pytest.mark.parametrize("variant", VARIANTS)
|
|
120
|
+
def test_incremental_matches_one_shot(variant):
|
|
121
|
+
full = b"hello world"
|
|
122
|
+
state = c.crc8(full[:5], variant)
|
|
123
|
+
state = c.crc8(full[5:], variant, init=state)
|
|
124
|
+
assert state == c.crc8(full, variant)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@pytest.mark.parametrize("variant", VARIANTS)
|
|
128
|
+
def test_incremental_byte_by_byte(variant):
|
|
129
|
+
data = b"the quick brown fox"
|
|
130
|
+
full = c.crc8(data, variant)
|
|
131
|
+
s = None
|
|
132
|
+
for b in data:
|
|
133
|
+
if s is None:
|
|
134
|
+
s = c.crc8(bytes([b]), variant)
|
|
135
|
+
else:
|
|
136
|
+
s = c.crc8(bytes([b]), variant, init=s)
|
|
137
|
+
assert s == full
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ───────────── CRC8 class ─────────────
|
|
141
|
+
|
|
142
|
+
@pytest.mark.parametrize("variant,expected", [
|
|
143
|
+
("crc-8", 0xF4),
|
|
144
|
+
("crc-8-darc", 0x15),
|
|
145
|
+
("crc-8-i-code", 0x7E),
|
|
146
|
+
("crc-8-itu", 0xA1),
|
|
147
|
+
("crc-8-maxim", 0xA1),
|
|
148
|
+
("crc-8-rohc", 0xD0),
|
|
149
|
+
("crc-8-wcdma", 0x25),
|
|
150
|
+
])
|
|
151
|
+
def test_class_matches_function_on_123456789(variant, expected):
|
|
152
|
+
h = c.CRC8(variant=variant)
|
|
153
|
+
h.update(b"123456789")
|
|
154
|
+
assert c.crc8(b"123456789", variant) == expected
|
|
155
|
+
assert h.digest() == bytes([expected])
|
|
156
|
+
assert h.hexdigest() == f"{expected:02x}"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_class_incremental_maxim():
|
|
160
|
+
expected = c.crc8(b"hello", "crc-8-maxim")
|
|
161
|
+
h = c.CRC8(variant="crc-8-maxim")
|
|
162
|
+
h.update(b"hel")
|
|
163
|
+
h.update(b"lo")
|
|
164
|
+
assert h.digest() == bytes([expected])
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_class_hexdigest_is_lowercase_two_chars():
|
|
168
|
+
h = c.CRC8(variant="crc-8")
|
|
169
|
+
h.update(b"\x00")
|
|
170
|
+
result = h.hexdigest()
|
|
171
|
+
assert len(result) == 2
|
|
172
|
+
assert result == result.lower()
|
|
173
|
+
assert all(ch in "0123456789abcdef" for ch in result)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_class_empty_maxim():
|
|
177
|
+
h = c.CRC8(variant="crc-8-maxim")
|
|
178
|
+
assert h.digest() == b"\x00"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_class_empty_rohc():
|
|
182
|
+
h = c.CRC8(variant="crc-8-rohc")
|
|
183
|
+
assert h.digest() == b"\xff"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ───────────── Range + boundary ─────────────
|
|
187
|
+
|
|
188
|
+
@pytest.mark.parametrize("variant", VARIANTS)
|
|
189
|
+
def test_single_byte_0x00(variant):
|
|
190
|
+
expected = c.crc8(b"\x00", variant)
|
|
191
|
+
assert 0 <= expected <= 255
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@pytest.mark.parametrize("variant", VARIANTS)
|
|
195
|
+
def test_single_byte_0xff(variant):
|
|
196
|
+
expected = c.crc8(b"\xff", variant)
|
|
197
|
+
assert 0 <= expected <= 255
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@pytest.mark.parametrize("variant", VARIANTS)
|
|
201
|
+
def test_returns_int_type(variant):
|
|
202
|
+
assert isinstance(c.crc8(b"abc", variant), int)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@pytest.mark.parametrize("variant", VARIANTS)
|
|
206
|
+
def test_large_input_1kb(variant):
|
|
207
|
+
data = b"x" * 1024
|
|
208
|
+
assert 0 <= c.crc8(data, variant) <= 255
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@pytest.mark.parametrize("variant", VARIANTS)
|
|
212
|
+
def test_large_input_64kb(variant):
|
|
213
|
+
data = bytes(range(256)) * 256
|
|
214
|
+
assert 0 <= c.crc8(data, variant) <= 255
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ───────────── Module metadata ─────────────
|
|
218
|
+
|
|
219
|
+
def test_module_has_version():
|
|
220
|
+
assert hasattr(c, "__version__")
|
|
221
|
+
assert c.__version__ == "0.1.0"
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def test_module_importable():
|
|
225
|
+
import importlib
|
|
226
|
+
m = importlib.import_module("crc8_pure")
|
|
227
|
+
assert m is c
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_seven_variants_implemented():
|
|
231
|
+
expected = {"crc-8", "crc-8-darc", "crc-8-i-code", "crc-8-itu",
|
|
232
|
+
"crc-8-maxim", "crc-8-rohc", "crc-8-wcdma"}
|
|
233
|
+
assert set(c.VARIANTS.keys()) == expected
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# ───────────── CLI ─────────────
|
|
237
|
+
|
|
238
|
+
@pytest.mark.parametrize("variant,data,expected", [
|
|
239
|
+
("crc-8", "123456789", "f4"),
|
|
240
|
+
("crc-8-maxim", "123456789", "a1"),
|
|
241
|
+
("crc-8-darc", "a", "1b"),
|
|
242
|
+
("crc-8-itu", "123456789", "a1"),
|
|
243
|
+
])
|
|
244
|
+
def test_cli_string_input(variant, data, expected):
|
|
245
|
+
result = subprocess.run(
|
|
246
|
+
[sys.executable, "-m", "crc8_pure", variant, data],
|
|
247
|
+
capture_output=True, text=True, check=True,
|
|
248
|
+
)
|
|
249
|
+
assert result.stdout.strip() == expected
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def test_cli_hex_flag():
|
|
253
|
+
result = subprocess.run(
|
|
254
|
+
[sys.executable, "-m", "crc8_pure", "crc-8", "123456789", "--hex"],
|
|
255
|
+
capture_output=True, text=True, check=True,
|
|
256
|
+
)
|
|
257
|
+
assert result.stdout.strip() == "0xf4"
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def test_cli_file_input(tmp_path):
|
|
261
|
+
f = tmp_path / "data.bin"
|
|
262
|
+
f.write_bytes(b"123456789")
|
|
263
|
+
result = subprocess.run(
|
|
264
|
+
[sys.executable, "-m", "crc8_pure", "crc-8", str(f)],
|
|
265
|
+
capture_output=True, text=True, check=True,
|
|
266
|
+
)
|
|
267
|
+
assert result.stdout.strip() == "f4"
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_cli_stdin_input():
|
|
271
|
+
result = subprocess.run(
|
|
272
|
+
[sys.executable, "-m", "crc8_pure", "crc-8", "-"],
|
|
273
|
+
input=b"123456789", capture_output=True, check=True,
|
|
274
|
+
)
|
|
275
|
+
assert result.stdout == b"f4\n"
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def test_cli_help_exits_zero():
|
|
279
|
+
result = subprocess.run(
|
|
280
|
+
[sys.executable, "-m", "crc8_pure", "--help"],
|
|
281
|
+
capture_output=True, text=True,
|
|
282
|
+
)
|
|
283
|
+
assert result.returncode == 0
|
|
284
|
+
assert "variant" in result.stdout.lower()
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# ───────────── Determinism ─────────────
|
|
288
|
+
|
|
289
|
+
@pytest.mark.parametrize("variant", VARIANTS)
|
|
290
|
+
def test_deterministic_same_input(variant):
|
|
291
|
+
a = c.crc8(b"reproducibility-check", variant)
|
|
292
|
+
b = c.crc8(b"reproducibility-check", variant)
|
|
293
|
+
assert a == b
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# ───────────── Additional: bit-flips change output ─────────────
|
|
297
|
+
|
|
298
|
+
@pytest.mark.parametrize("variant", VARIANTS)
|
|
299
|
+
def test_bit_flip_changes_crc(variant):
|
|
300
|
+
base = c.crc8(b"hello", variant)
|
|
301
|
+
flipped = c.crc8(b"hellp", variant) # 'o'=0x6F -> 'p'=0x70
|
|
302
|
+
assert base != flipped
|