diff cutils/treesum.py @ 179:53614a724bf0

Also write a (standard) CRC-32 checksum for each block of output
author Franz Glasner <fzglas.hg@dom66.de>
date Sat, 11 Jan 2025 22:26:00 +0100
parents dac26a2d9de5
children 2784fdcc99e5
line wrap: on
line diff
--- a/cutils/treesum.py	Sat Jan 11 21:18:53 2025 +0100
+++ b/cutils/treesum.py	Sat Jan 11 22:26:00 2025 +0100
@@ -22,6 +22,7 @@
 import stat
 import sys
 import time
+import zlib
 
 from . import (__version__, __revision__)
 from . import util
@@ -278,6 +279,7 @@
             out_cm = open(opts.output, "ab")
         else:
             out_cm = open(opts.output, "wb")
+    out_cm = CRC32Output(out_cm)
 
     with out_cm as outfp:
         for d in opts.directories:
@@ -321,6 +323,7 @@
 
         """
         self._outfp = outfp
+        self._outfp.resetdigest()
         self._outfp.write(format_bsd_line("VERSION", "1", None, False))
         self._outfp.flush()
 
@@ -394,9 +397,13 @@
                         "./@",
                         self._use_base64))
             self._outfp.flush()
+            self._outfp.write(format_bsd_line(
+                "CRC32", self._outfp.hexcrcdigest(), None, False))
             return
 
         self._generate(os.path.normpath(root), tuple())
+        self._outfp.write(format_bsd_line(
+                "CRC32", self._outfp.hexcrcdigest(), None, False))
 
     def _generate(self, root, top):
         logging.debug("Handling %s/%r", root, top)
@@ -522,6 +529,60 @@
         return (dir_dgst.digest(), dir_size)
 
 
+class CRC32Output(object):
+
+    """Wrapper for a minimal binary file contextmanager that calculates
+    the CRC32 of the written bytes on the fly.
+
+    Also acts as context manager proxy for the given context manager.
+
+    """
+
+    __slots__ = ("_fp_cm", "_fp", "_crc32")
+
+    def __init__(self, fp_cm):
+        self._fp_cm = fp_cm
+        self._fp = None
+        self.resetdigest()
+
+    def __enter__(self):
+        assert self._fp is None
+        self._fp = self._fp_cm.__enter__()
+        return self
+
+    def __exit__(self, *args):
+        rv = self._fp_cm.__exit__(*args)
+        self._fp = None
+        return rv
+
+    def write(self, what):
+        self._fp.write(what)
+        self._crc32 = zlib.crc32(what, self._crc32)
+
+    def flush(self):
+        self._fp.flush()
+
+    def resetdigest(self):
+        """Reset the current CRC digest"""
+        self._crc32 = zlib.crc32(b"")
+
+    def hexcrcdigest(self):
+        """
+
+        :rtype: str
+
+        """
+        return (hex(self._crc32)[2:]).upper()
+
+    def crcdigest(self):
+        """
+
+        :rtype: int
+
+        """
+        return self._crc32
+
+
 def normalized_compatible_mode_str(mode):
     # XXX FIXME: Windows and "executable"
     modebits = stat.S_IMODE(mode)
@@ -546,7 +607,7 @@
     if what == b"TIMESTAMP":
         assert filename is None
         return util.interpolate_bytes(b"TIMESTAMP = %d%s", value, ls)
-    if what in (b"ISOTIMESTAMP", b"FLAGS", b"VERSION"):
+    if what in (b"ISOTIMESTAMP", b"FLAGS", b"VERSION", b"CRC32"):
         assert filename is None
         if not isinstance(value, bytes):
             value = value.encode("ascii")