Skip to content

Commit e527a77

Browse files
authored
Merge pull request #29 from MatrixEditor/dec/fix-py3.14-digest
[FIX] Python 3.14 Digest alternative
2 parents 78cdc60 + ee74099 commit e527a77

12 files changed

Lines changed: 341 additions & 59 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
1515
> [!NOTE]
1616
> Python 3.14 breaks `with` statements in class definitions since `__annotations__` are added at the end
17-
> of a class definition. Therefore, `Digest` and conditional statements **ARE NOT SUPPORTED** in Python 3.14+.
17+
> of a class definition. Therefore, `Digest` and conditional statements **ARE NOT SUPPORTED** in using the `with` syntax Python 3.14+.
18+
> As of version `2.4.5` the `Digest` class has a counterpart (`DigestField`), which can be used to manually specify a digest without
19+
> the need of a `ẁith` statement.
1820
1921

2022

docs/sphinx/source/library/fields/crypto.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ Hashes
1313

1414
.. autoclass:: caterpillar.py.Digest
1515

16+
.. autoclass:: caterpillar.py.DigestField
17+
18+
.. autoclass:: caterpillar.py.DigestFieldAction
19+
1620

1721
Ciphers
1822
-------

docs/sphinx/source/tutorial/advanced/actions.rst

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,43 @@ Advanced Usage: Message Digests
3131
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3232

3333
.. warning::
34-
This feature is not supported in Python 3.14+.
34+
This feature has a different syntax in Python 3.14+.
3535

3636
Actions can also be used to perform more complex operations, such as calculating
3737
checksums or cryptographic hash values before or after processing fields. For
3838
example, you can use an action to automatically wrap a sequence of fields with a
3939
specialized message digest like SHA256 (see :class:`~caterpillar.py.Digest`):
4040

41-
.. code-block:: python
41+
.. tab-set::
42+
43+
.. tab-item:: Python <= 3.13
44+
45+
.. code-block:: python
46+
47+
@struct
48+
class Format:
49+
key: b"..."
50+
with Sha2_256("hash", verify=True):
51+
user_data: Bytes(50)
52+
# the 'hash' attribute will be set automatically
53+
54+
55+
.. tab-item:: Python >= 3.14
56+
57+
.. code-block:: python
58+
59+
@struct
60+
class Format:
61+
key: b"..."
62+
_hash_begin: DigestField.begin("hash", Sha2_256_Algo)
63+
user_data: Bytes(50)
64+
# the 'hash' attribute must be set manually
65+
hash: DigestField("hash", Bytes(32), verify=True) = None
66+
# or
67+
hash: Sha2_256_Field("hash", verify=True) = None
68+
69+
4270
43-
@struct
44-
class Format:
45-
a: uint8
46-
with Sha2_256("hash", verify=True):
47-
user_data: Bytes(50)
48-
# the 'hash' attribute will be set automatically
4971
5072
In this example, the :code:`user_data` field is wrapped with the :code:`Sha2_256` digest action.
5173
When the struct is packed, a SHA256 hash is computed for the :code:`user_data` and stored
@@ -54,17 +76,36 @@ with, the verification step will raise an error.
5476

5577
The resulting struct includes the following fields:
5678

57-
.. code-block:: python
5879

59-
>>> Format.__struct__.fields
60-
[
61-
Field('key', struct=<ConstBytes>, ...),
62-
(Action(Digest.begin), None),
63-
Field('user_data', struct=<Bytes>, ...),
64-
(Action(Digest.end_pack, Digest.end_unpack), None),
65-
Field('hash', struct=<Bytes>, ...),
66-
(UnpackAction(Digest.verfiy), None)
67-
]
80+
.. tab-set::
81+
82+
.. tab-item:: Python <= 3.13
83+
84+
.. code-block:: python
85+
86+
>>> Format.__struct__.fields
87+
[
88+
Field('key', struct=<ConstBytes>, ...),
89+
(Action(Digest.begin), None),
90+
Field('user_data', struct=<Bytes>, ...),
91+
(Action(Digest.end_pack, Digest.end_unpack), None),
92+
Field('hash', struct=<Bytes>, ...),
93+
(UnpackAction(Digest.verfiy), None)
94+
]
95+
96+
97+
.. tab-item:: Python >= 3.14
98+
99+
.. code-block:: python
100+
101+
>>> Format.__struct__.fields
102+
[
103+
Field('key', struct=<ConstBytes>, ...),
104+
(<DigestFieldAction>, None),
105+
Field('user_data', struct=<Bytes>, ...),
106+
<DigestField>,
107+
]
108+
68109
69110
Here, you can see that:
70111

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ cmake.source-dir = "."
1010

1111
[project]
1212
name = "caterpillar"
13-
version = "2.4.4"
13+
version = "2.4.5"
1414

1515
description="Library to pack and unpack structurized binary data."
1616
authors = [

src/caterpillar/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
# along with this program. If not, see <https://www.gnu.org/licenses/>.
1515
import warnings
1616

17-
__version__ = "2.4.4"
17+
__version__ = "2.4.5"
1818
__release__ = None
1919
__author__ = "MatrixEditor"
2020

src/caterpillar/abc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ class _SupportsUnpack(Protocol):
9797
"""
9898

9999
@abstractmethod
100-
def __unpack__(self, context: _ContextLike):
100+
def __unpack__(self, context: _ContextLike) -> Any:
101101
pass
102102

103103

@@ -123,7 +123,7 @@ def __size__(self, context: _ContextLike) -> int:
123123
pass
124124

125125
@abstractmethod
126-
def __unpack__(self, context: _ContextLike):
126+
def __unpack__(self, context: _ContextLike) -> Any:
127127
pass
128128

129129
@abstractmethod

src/caterpillar/fields/__init__.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,30 @@
8484
Crc32,
8585
Adler,
8686
HMAC,
87+
DigestField,
88+
DigestFieldAction,
89+
Md5_Algo,
90+
Md5_Field,
91+
Sha1_Algo,
92+
Sha1_Field,
93+
Sha2_256_Algo,
94+
Sha2_256_Field,
95+
Sha2_224_Algo,
96+
Sha2_224_Field,
97+
Sha2_384_Algo,
98+
Sha2_384_Field,
99+
Sha2_512_Algo,
100+
Sha2_512_Field,
101+
Sha3_224_Algo,
102+
Sha3_224_Field,
103+
Sha3_256_Algo,
104+
Sha3_256_Field,
105+
Sha3_384_Algo,
106+
Sha3_384_Field,
107+
Sha3_512_Algo,
108+
Sha3_512_Field,
109+
Crc32_Algo,
110+
Crc32_Field,
111+
Adler_Algo,
112+
Adler_Field,
87113
)

src/caterpillar/fields/_base.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
#
1313
# You should have received a copy of the GNU General Public License
1414
# along with this program. If not, see <https://www.gnu.org/licenses/>.
15-
from typing import Self, Union, Set, Any, Dict, Optional, List, Callable
15+
from typing import Self, Union, Set, Any, Dict, Optional, List
1616
from io import BytesIO
1717
from caterpillar.abc import (
1818
_StructLike,
@@ -42,7 +42,7 @@
4242
Flag,
4343
)
4444
from caterpillar.context import CTX_OFFSETS, CTX_STREAM
45-
from caterpillar.context import CTX_FIELD, CTX_POS
45+
from caterpillar.context import CTX_FIELD
4646
from caterpillar.context import CTX_VALUE, CTX_SEQ
4747
from caterpillar import registry
4848

@@ -409,7 +409,7 @@ def __unpack__(self, context: _ContextLike) -> Optional[Any]:
409409
# The "keep_position" flag is not applicable here. Configure a field to keep the
410410
# position afterward.
411411
context[CTX_VALUE] = value
412-
return struct.__unpack__(context)
412+
value = struct.__unpack__(context)
413413

414414
return value
415415

0 commit comments

Comments
 (0)