comparison 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
comparison
equal deleted inserted replaced
178:dac26a2d9de5 179:53614a724bf0
20 import logging 20 import logging
21 import os 21 import os
22 import stat 22 import stat
23 import sys 23 import sys
24 import time 24 import time
25 import zlib
25 26
26 from . import (__version__, __revision__) 27 from . import (__version__, __revision__)
27 from . import util 28 from . import util
28 from .util import cm 29 from .util import cm
29 from .util import digest 30 from .util import digest
276 else: 277 else:
277 if opts.append_output: 278 if opts.append_output:
278 out_cm = open(opts.output, "ab") 279 out_cm = open(opts.output, "ab")
279 else: 280 else:
280 out_cm = open(opts.output, "wb") 281 out_cm = open(opts.output, "wb")
282 out_cm = CRC32Output(out_cm)
281 283
282 with out_cm as outfp: 284 with out_cm as outfp:
283 for d in opts.directories: 285 for d in opts.directories:
284 286
285 V1DirectoryTreesumGenerator( 287 V1DirectoryTreesumGenerator(
319 321
320 :param outfp: a *binary* file with a "write()" and a "flush()" method 322 :param outfp: a *binary* file with a "write()" and a "flush()" method
321 323
322 """ 324 """
323 self._outfp = outfp 325 self._outfp = outfp
326 self._outfp.resetdigest()
324 self._outfp.write(format_bsd_line("VERSION", "1", None, False)) 327 self._outfp.write(format_bsd_line("VERSION", "1", None, False))
325 self._outfp.flush() 328 self._outfp.flush()
326 329
327 # 330 #
328 # Note: Given non-default flags that are relevant for 331 # Note: Given non-default flags that are relevant for
392 self._algorithm[1], 395 self._algorithm[1],
393 dir_dgst.digest(), 396 dir_dgst.digest(),
394 "./@", 397 "./@",
395 self._use_base64)) 398 self._use_base64))
396 self._outfp.flush() 399 self._outfp.flush()
400 self._outfp.write(format_bsd_line(
401 "CRC32", self._outfp.hexcrcdigest(), None, False))
397 return 402 return
398 403
399 self._generate(os.path.normpath(root), tuple()) 404 self._generate(os.path.normpath(root), tuple())
405 self._outfp.write(format_bsd_line(
406 "CRC32", self._outfp.hexcrcdigest(), None, False))
400 407
401 def _generate(self, root, top): 408 def _generate(self, root, top):
402 logging.debug("Handling %s/%r", root, top) 409 logging.debug("Handling %s/%r", root, top)
403 path = os.path.join(root, *top) if top else root 410 path = os.path.join(root, *top) if top else root
404 with walk.ScanDir(path) as dirscan: 411 with walk.ScanDir(path) as dirscan:
520 self._use_base64)) 527 self._use_base64))
521 self._outfp.flush() 528 self._outfp.flush()
522 return (dir_dgst.digest(), dir_size) 529 return (dir_dgst.digest(), dir_size)
523 530
524 531
532 class CRC32Output(object):
533
534 """Wrapper for a minimal binary file contextmanager that calculates
535 the CRC32 of the written bytes on the fly.
536
537 Also acts as context manager proxy for the given context manager.
538
539 """
540
541 __slots__ = ("_fp_cm", "_fp", "_crc32")
542
543 def __init__(self, fp_cm):
544 self._fp_cm = fp_cm
545 self._fp = None
546 self.resetdigest()
547
548 def __enter__(self):
549 assert self._fp is None
550 self._fp = self._fp_cm.__enter__()
551 return self
552
553 def __exit__(self, *args):
554 rv = self._fp_cm.__exit__(*args)
555 self._fp = None
556 return rv
557
558 def write(self, what):
559 self._fp.write(what)
560 self._crc32 = zlib.crc32(what, self._crc32)
561
562 def flush(self):
563 self._fp.flush()
564
565 def resetdigest(self):
566 """Reset the current CRC digest"""
567 self._crc32 = zlib.crc32(b"")
568
569 def hexcrcdigest(self):
570 """
571
572 :rtype: str
573
574 """
575 return (hex(self._crc32)[2:]).upper()
576
577 def crcdigest(self):
578 """
579
580 :rtype: int
581
582 """
583 return self._crc32
584
585
525 def normalized_compatible_mode_str(mode): 586 def normalized_compatible_mode_str(mode):
526 # XXX FIXME: Windows and "executable" 587 # XXX FIXME: Windows and "executable"
527 modebits = stat.S_IMODE(mode) 588 modebits = stat.S_IMODE(mode)
528 modestr = "%o" % (modebits,) 589 modestr = "%o" % (modebits,)
529 if not modestr.startswith("0"): 590 if not modestr.startswith("0"):
544 if not isinstance(what, bytes): 605 if not isinstance(what, bytes):
545 what = what.encode("ascii") 606 what = what.encode("ascii")
546 if what == b"TIMESTAMP": 607 if what == b"TIMESTAMP":
547 assert filename is None 608 assert filename is None
548 return util.interpolate_bytes(b"TIMESTAMP = %d%s", value, ls) 609 return util.interpolate_bytes(b"TIMESTAMP = %d%s", value, ls)
549 if what in (b"ISOTIMESTAMP", b"FLAGS", b"VERSION"): 610 if what in (b"ISOTIMESTAMP", b"FLAGS", b"VERSION", b"CRC32"):
550 assert filename is None 611 assert filename is None
551 if not isinstance(value, bytes): 612 if not isinstance(value, bytes):
552 value = value.encode("ascii") 613 value = value.encode("ascii")
553 return util.interpolate_bytes(b"%s = %s%s", what, value, ls) 614 return util.interpolate_bytes(b"%s = %s%s", what, value, ls)
554 assert filename is not None 615 assert filename is not None