changeset 255:d852559df523

Wrap zlib.crc32 into a hashlib-compatible interface
author Franz Glasner <fzglas.hg@dom66.de>
date Sat, 15 Feb 2025 16:36:05 +0100
parents 655f9e4bc6f2
children d50e89d92fa3
files cutils/util/crc32.py tests/test_crc32.py
diffstat 2 files changed, 216 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cutils/util/crc32.py	Sat Feb 15 16:36:05 2025 +0100
@@ -0,0 +1,90 @@
+# -*- coding: utf-8 -*-
+# :-
+# :Copyright: (c) 2025 Franz Glasner
+# :License:   BSD-3-Clause
+# :-
+r"""Wrap :func:`zlib.crc32` into a :mod:`hashlib` compatible interface.
+
+"""
+
+from __future__ import print_function, absolute_import
+
+
+__all__ = ["crc32"]
+
+
+import struct
+import zlib
+
+from . import PY2
+
+
+try:
+    long
+except NameError:
+    INT_TYPES = (int, )
+else:
+    INT_TYPES = (int, long)            # noqa: F821  long is undefined
+
+
+class crc32(object):
+
+    __slots__ = ["_crc"]
+
+    zlib_crc32 = zlib.crc32
+
+    def __init__(self, data=b""):
+        self._crc = self.zlib_crc32(data)
+
+    def update(self, data):
+        self._crc = self.zlib_crc32(data, self._crc)
+
+    def digest(self):
+        return struct.pack(">I", self._get_crc_as_uint32())
+
+    def hexdigest(self):
+        return "{:08X}".format(self._get_crc_as_uint32())
+
+    def copy(self):
+        copied_crc = crc32()
+        copied_crc._crc = self._crc
+        return copied_crc
+
+    def __eq__(self, other):
+        if isinstance(other, crc32):
+            return self._crc == other._crc
+        elif isinstance(other, INT_TYPES):
+            return self._get_crc_as_uint32() == self.as_uint32(other)
+        else:
+            return NotImplemented
+
+    def __ne__(self, other):
+        equal = self.__eq__(other)
+        return equal if equal is NotImplemented else not equal
+
+    def _get_crc_as_uint32(self):
+        """Get the current CRC always as positive number with the same bit
+        pattern because Python2 yields negative numbers also.
+
+        :return: The current CRC value as positive number on all Python
+                 versions
+        :rtype: int
+
+        """
+        if PY2:
+            if self._crc < 0:
+                # Return the bitpattern as unsigned 32-bit number
+                return (~self._crc ^ 0xFFFFFFFF)
+            else:
+                return self._crc
+        else:
+            return self._crc
+
+    @staticmethod
+    def as_uint32(i):
+        if i < 0:
+            if i < -2147483648:
+                raise OverflowError("")
+            return (~i ^ 0xFFFFFFFF)
+        else:
+            return i
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_crc32.py	Sat Feb 15 16:36:05 2025 +0100
@@ -0,0 +1,126 @@
+# -*- coding: utf-8 -*-
+r"""Unit tests for the vendored crcmod2 package.
+
+"""
+
+from __future__ import absolute_import, print_function
+
+import struct
+import sys
+import unittest
+import zlib
+
+import _test_setup           # noqa: F401 imported but unused
+
+import cutils.util.crc32
+
+
+class TestCRC32(unittest.TestCase):
+
+    def test_empty(self):
+        c1 = cutils.util.crc32.crc32()
+        c2 = zlib.crc32(b"")
+        self.assertEqual(c1._crc, c2)
+        self.assertEqual("00000000", c1.hexdigest())
+        self.assertEqual(b"\x00\x00\x00\x00", c1.digest())
+
+    def test_uint32(self):
+        c1 = cutils.util.crc32.crc32()
+        c1._crc = 0xffffffff
+        self.assertEqual("FFFFFFFF", c1.hexdigest())
+        self.assertEqual(b"\xff\xff\xff\xff", c1.digest())
+
+    @unittest.skipIf(sys.version_info[0] >= 3, "Only for Python 2")
+    def test_negative_minus_one(self):
+        # Python2 yields negative numbers in zlib.crc32()
+        c1 = cutils.util.crc32.crc32()
+        c1._crc = -1
+        self.assertEqual("FFFFFFFF", c1.hexdigest())
+        self.assertEqual(b"\xff\xff\xff\xff", c1.digest())
+
+    def test_postitive_asd(self):
+        c1 = cutils.util.crc32.crc32(b"asd\n")
+        c2 = zlib.crc32(b"")
+        c2 = zlib.crc32(b"asd\n", c2)
+        self.assertEqual(c1._crc, c2)
+        self.assertEqual(355327694, c1._crc)
+        self.assertEqual("152DDECE", c1.hexdigest())
+        self.assertEqual(b"\x15\x2d\xde\xce", c1.digest())
+
+    def test_negative_123456789(self):
+        c1 = cutils.util.crc32.crc32()
+        c1.update(b"123456789")
+        c2 = zlib.crc32(b"123456789")
+        self.assertEqual(c1._crc, c2)
+        self.assertEqual("CBF43926", c1.hexdigest())
+        self.assertEqual(b"\xcb\xf4\x39\x26", c1.digest())
+
+    def test_copy(self):
+        c1 = cutils.util.crc32.crc32(b"123456789")
+        c2 = c1.copy()
+        self.assertEqual(c1._crc, c2._crc)
+        self.assertEqual("CBF43926", c2.hexdigest())
+
+    def test_copy_parts(self):
+        c1 = cutils.util.crc32.crc32(b"12345")
+        c2 = c1.copy()
+        c2.update(b"6789")
+        self.assertEqual("CBF43926", c2.hexdigest())
+
+    def test_equality(self):
+        c1 = cutils.util.crc32.crc32(b"123")
+        c2 = c1.copy()
+        c3 = zlib.crc32(b"123")
+        self.assertEqual(c2, c1)
+        self.assertEqual(c1, c2._crc)
+        self.assertEqual(c1, c3)
+        self.assertNotEqual(c1, None)
+
+    def test_equality_2(self):
+        c1 = cutils.util.crc32.crc32(b"123456789")
+        c2 = c1.copy()
+        c3 = zlib.crc32(b"123456789")
+        self.assertEqual(c2, c1)
+        self.assertEqual(c1, c2._crc)
+        self.assertEqual(c1, c3)
+        self.assertNotEqual(c1, None)
+
+    def test_inequality(self):
+        c1 = cutils.util.crc32.crc32(b"123")
+        c2 = c1.copy()
+        c1.update(b"4")
+        c3 = zlib.crc32(b"12345")
+        self.assertNotEqual(c2, c1)
+        self.assertNotEqual(c1, c2._crc)
+        self.assertNotEqual(c1, c3)
+        self.assertNotEqual(c1, None)
+
+    def test_inequality_2(self):
+        c1 = cutils.util.crc32.crc32(b"12345678")
+        c2 = c1.copy()
+        c2.update(b"9")
+        c3 = zlib.crc32(b"1234567")
+        self.assertNotEqual(c2, c1)
+        self.assertNotEqual(c1, c2._crc)
+        self.assertNotEqual(c1, c3)
+        self.assertNotEqual(c1, None)
+
+    def test_equality_negative_number(self):
+        c1 = cutils.util.crc32.crc32(b"123456789")
+        # Py2 yields signed ints
+        self.assertEqual(c1, -873187034)
+        # Py3 yields unsigned ints
+        self.assertEqual(c1, 3421780262)
+
+    def test_as_uint32(self):
+        self.assertEqual(3421780262,
+                         cutils.util.crc32.crc32.as_uint32(-873187034))
+
+    def test_struct_pack(self):
+        self.assertEqual(
+            struct.pack(">I", 3421780262),
+            struct.pack(">i", -873187034))
+
+
+if __name__ == "__main__":
+    unittest.main()