# HG changeset patch # User Franz Glasner # Date 1738928294 -3600 # Node ID a828f320ac583fd40623f5988302df0974976e87 # Parent 5d992e2a2fbc4bd8e01b65ad4c35ad2ded9658f1# Parent 4bb2d0975cfe454c01a6cb5690ca65953efed082 MERGE: with branch "imports": genpwd.py diff -r 4bb2d0975cfe -r a828f320ac58 .hgignore --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,8 @@ +syntax: regexp + +(^|/)_venv.* +\.py[co]$ +^dist/ +^build/ +^py_cutils\.egg\-info/ +^__.* diff -r 4bb2d0975cfe -r a828f320ac58 .hgkwarchive --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgkwarchive Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,3 @@ +[patterns] +path:cutils/__init__.py = RCS, reST +path:README.txt = RCS, reST diff -r 4bb2d0975cfe -r a828f320ac58 .hgsigs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgsigs Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,13 @@ +ed7e5010990c4e496e4a42f08f08cbd490289250 0 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 diff -r 4bb2d0975cfe -r a828f320ac58 .hgtags --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgtags Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,17 @@ +409dac636805b9384762f02db7dd053e7ea2d629 v0.1 +b80519d583457bbde9c54ab5efe9e1c85f1dea8b v0.2 +801f6585a263fb488cc026d5a579890012d7860d v0.3 +801f6585a263fb488cc026d5a579890012d7860d v0.3 +0000000000000000000000000000000000000000 v0.3 +0000000000000000000000000000000000000000 v0.3 +acedf239289df7a8183098a4b95638c41fa5a9d3 v0.3 +7a3bb86e2d155fc3167a003f60694a1a7c9b9447 v0.3.1 +2e0cf1e7c4833ba462fc31be5082878536a6854f v0.3.2 +44172581bfb8c465f84be783a7cfc5d4b2746c7d v0.3.3 +15c1058c832e8a2c1e9e53c32b578b57379008ac v0.4 +79f49b0602c0dc9b3972f36d34451cda52656d05 v0.4.1 +0ebdd6b01c0854f779d02f407165985afa9edcf0 v0.4.2 +10785998be38ea66181d892baaea548dcba9f24a v0.4.3 +0edbb9a261f7e615822e8635f16784f31d0b9a0c v0.4.4 +798c79420f6595a745ad691ecf68bc858a51197b v0.5 +422a594c588ee6f634cf3864629a1bbf00226ae7 v0.5.1 diff -r 4bb2d0975cfe -r a828f320ac58 LICENSE.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/LICENSE.txt Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,59 @@ +Copyright (c) 2020-2025 Franz Glasner + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the + distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +Licenses for incorporated software +================================== + +crcmod/crcmod2 -- vendored in cutils.crcmod -- MIT License +---------------------------------------------------------- + +Copyright (c) 2010 Raymond L. Buvel +Copyright (c) 2010 Craig McQueen +Copyright (c) 2025 Franz Glasner + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +---------------------------------------------------------------------------- diff -r 4bb2d0975cfe -r a828f320ac58 MANIFEST.in --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MANIFEST.in Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,5 @@ +include .hg* *.txt *.py +graft docs +prune docs/_build +graft tests +global-exclude *.pyc *.pyo diff -r 4bb2d0975cfe -r a828f320ac58 README.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/README.txt Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,21 @@ +=========== + py-cutils +=========== + +:Author: Franz Glasner +:Version: 0.5.1 +:Date: 2025-01-25 +:Copyright: (c) 2020-2025 Franz Glasner +:License: BSD 3-Clause "New" or "Revised" License +:ID: @(#) $HGid$ + +Pure Python implementations of coreutils and some additional utilities. + +Currently implemented: + +- `dos2unix` as :command:`py-dos2unix` +- `shasum` as :command:`py-shasum` + +Additional utilities: + +- :command:`py-treesum` to compute hash trees for directories. diff -r 4bb2d0975cfe -r a828f320ac58 cutils/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cutils/__init__.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +r""" +:Author: Franz Glasner +:Copyright: (c) 2020-2025 Franz Glasner +:License: BSD 3-Clause "New" or "Revised" License. + See :ref:`LICENSE.txt ` for details. + If you cannot find LICENSE.txt see + +:ID: @(#) $HGid$ + +""" + +__version__ = "0.5.1" + +__revision__ = "|VCSRevision|" +__date__ = "|VCSJustDate|" + +__all__ = ["__version__"] diff -r 4bb2d0975cfe -r a828f320ac58 cutils/crcmod/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cutils/crcmod/__init__.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,41 @@ +# -*- coding: utf-8; -*- +#----------------------------------------------------------------------------- +# Copyright (c) 2010 Raymond L. Buvel +# Copyright (c) 2010 Craig McQueen +# Copyright (c) 2025 Franz Glasner +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +#----------------------------------------------------------------------------- + +from __future__ import absolute_import + +import sys + +if sys.version_info[0] < 3: + from .python2.crcmod import * + from .python2.crcmod import __doc__ +else: + from .python3.crcmod import * + from .python3.crcmod import __doc__ + + +__version__ = "1.7" + + +del sys diff -r 4bb2d0975cfe -r a828f320ac58 cutils/crcmod/predefined.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cutils/crcmod/predefined.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,37 @@ +# -*- coding: utf-8; -*- +#----------------------------------------------------------------------------- +# Copyright (c) 2010 Raymond L. Buvel +# Copyright (c) 2010 Craig McQueen +# Copyright (c) 2025 Franz Glasner +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +#----------------------------------------------------------------------------- + +from __future__ import absolute_import + +import sys + +if sys.version_info[0] < 3: + from .python2.predefined import * + from .python2.predefined import __doc__ +else: + from .python3.predefined import * + from .python3.predefined import __doc__ + +del sys diff -r 4bb2d0975cfe -r a828f320ac58 cutils/crcmod/python2/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cutils/crcmod/python2/__init__.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +#----------------------------------------------------------------------------- +# Copyright (c) 2010 Raymond L. Buvel +# Copyright (c) 2010 Craig McQueen +# Copyright (c) 2025 Franz Glasner +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +#----------------------------------------------------------------------------- diff -r 4bb2d0975cfe -r a828f320ac58 cutils/crcmod/python2/_crcfunpy.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cutils/crcmod/python2/_crcfunpy.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,87 @@ +#----------------------------------------------------------------------------- +# Low level CRC functions for use by crcmod. This version is implemented in +# Python for a couple of reasons. 1) Provide a reference implememtation. +# 2) Provide a version that can be used on systems where a C compiler is not +# available for building extension modules. +# +# Copyright (c) 2004 Raymond L. Buvel +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +#----------------------------------------------------------------------------- + +def _crc8(data, crc, table): + crc = crc & 0xFF + for x in data: + crc = table[ord(x) ^ crc] + return crc + +def _crc8r(data, crc, table): + crc = crc & 0xFF + for x in data: + crc = table[ord(x) ^ crc] + return crc + +def _crc16(data, crc, table): + crc = crc & 0xFFFF + for x in data: + crc = table[ord(x) ^ ((crc>>8) & 0xFF)] ^ ((crc << 8) & 0xFF00) + return crc + +def _crc16r(data, crc, table): + crc = crc & 0xFFFF + for x in data: + crc = table[ord(x) ^ (crc & 0xFF)] ^ (crc >> 8) + return crc + +def _crc24(data, crc, table): + crc = crc & 0xFFFFFF + for x in data: + crc = table[ord(x) ^ (int(crc>>16) & 0xFF)] ^ ((crc << 8) & 0xFFFF00) + return crc + +def _crc24r(data, crc, table): + crc = crc & 0xFFFFFF + for x in data: + crc = table[ord(x) ^ int(crc & 0xFF)] ^ (crc >> 8) + return crc + +def _crc32(data, crc, table): + crc = crc & 0xFFFFFFFFL + for x in data: + crc = table[ord(x) ^ (int(crc>>24) & 0xFF)] ^ ((crc << 8) & 0xFFFFFF00L) + return crc + +def _crc32r(data, crc, table): + crc = crc & 0xFFFFFFFFL + for x in data: + crc = table[ord(x) ^ int(crc & 0xFFL)] ^ (crc >> 8) + return crc + +def _crc64(data, crc, table): + crc = crc & 0xFFFFFFFFFFFFFFFFL + for x in data: + crc = table[ord(x) ^ (int(crc>>56) & 0xFF)] ^ ((crc << 8) & 0xFFFFFFFFFFFFFF00L) + return crc + +def _crc64r(data, crc, table): + crc = crc & 0xFFFFFFFFFFFFFFFFL + for x in data: + crc = table[ord(x) ^ int(crc & 0xFFL)] ^ (crc >> 8) + return crc + diff -r 4bb2d0975cfe -r a828f320ac58 cutils/crcmod/python2/crcmod.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cutils/crcmod/python2/crcmod.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,476 @@ +#----------------------------------------------------------------------------- +# Copyright (c) 2010 Raymond L. Buvel +# Copyright (c) 2010 Craig McQueen +# Copyright (c) 2025 Franz Glasner +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +#----------------------------------------------------------------------------- +'''crcmod is a Python module for gererating objects that compute the Cyclic +Redundancy Check. Any 8, 16, 24, 32, or 64 bit polynomial can be used. + +The following are the public components of this module. + +Crc -- a class that creates instances providing the same interface as the +md5 and sha modules in the Python standard library. These instances also +provide a method for generating a C/C++ function to compute the CRC. + +mkCrcFun -- create a Python function to compute the CRC using the specified +polynomial and initial value. This provides a much simpler interface if +all you need is a function for CRC calculation. +''' + +from __future__ import absolute_import + + +__all__ = '''mkCrcFun Crc'''.split() + + +# Select the appropriate set of low-level CRC functions for this installation. +# If the extension module was not built, drop back to the Python implementation +# even though it is significantly slower. +try: + from . import _crcfunext as _crcfun + _usingExtension = True +except ImportError: + from . import _crcfunpy as _crcfun + _usingExtension = False + +import sys, struct + +#----------------------------------------------------------------------------- +class Crc: + '''Compute a Cyclic Redundancy Check (CRC) using the specified polynomial. + + Instances of this class have the same interface as the md5 and sha modules + in the Python standard library. See the documentation for these modules + for examples of how to use a Crc instance. + + The string representation of a Crc instance identifies the polynomial, + initial value, XOR out value, and the current CRC value. The print + statement can be used to output this information. + + If you need to generate a C/C++ function for use in another application, + use the generateCode method. If you need to generate code for another + language, subclass Crc and override the generateCode method. + + The following are the parameters supplied to the constructor. + + poly -- The generator polynomial to use in calculating the CRC. The value + is specified as a Python integer or long integer. The bits in this integer + are the coefficients of the polynomial. The only polynomials allowed are + those that generate 8, 16, 24, 32, or 64 bit CRCs. + + initCrc -- Initial value used to start the CRC calculation. This initial + value should be the initial shift register value XORed with the final XOR + value. That is equivalent to the CRC result the algorithm should return for + a zero-length string. Defaults to all bits set because that starting value + will take leading zero bytes into account. Starting with zero will ignore + all leading zero bytes. + + rev -- A flag that selects a bit reversed algorithm when True. Defaults to + True because the bit reversed algorithms are more efficient. + + xorOut -- Final value to XOR with the calculated CRC value. Used by some + CRC algorithms. Defaults to zero. + ''' + def __init__(self, poly, initCrc=~0L, rev=True, xorOut=0, initialize=True): + if not initialize: + # Don't want to perform the initialization when using new or copy + # to create a new instance. + return + + (sizeBits, initCrc, xorOut) = _verifyParams(poly, initCrc, xorOut) + self.digest_size = sizeBits//8 + self.initCrc = initCrc + self.xorOut = xorOut + + self.poly = poly + self.reverse = rev + + (crcfun, table) = _mkCrcFun(poly, sizeBits, initCrc, rev, xorOut) + self._crc = crcfun + self.table = table + + self.crcValue = self.initCrc + + def __str__(self): + lst = [] + lst.append('poly = 0x%X' % self.poly) + lst.append('reverse = %s' % self.reverse) + fmt = '0x%%0%dX' % (self.digest_size*2) + lst.append('initCrc = %s' % (fmt % self.initCrc)) + lst.append('xorOut = %s' % (fmt % self.xorOut)) + lst.append('crcValue = %s' % (fmt % self.crcValue)) + return '\n'.join(lst) + + def new(self, arg=None): + '''Create a new instance of the Crc class initialized to the same + values as the original instance. The current CRC is set to the initial + value. If a string is provided in the optional arg parameter, it is + passed to the update method. + ''' + n = Crc(poly=None, initialize=False) + n._crc = self._crc + n.digest_size = self.digest_size + n.initCrc = self.initCrc + n.xorOut = self.xorOut + n.table = self.table + n.crcValue = self.initCrc + n.reverse = self.reverse + n.poly = self.poly + if arg is not None: + n.update(arg) + return n + + def copy(self): + '''Create a new instance of the Crc class initialized to the same + values as the original instance. The current CRC is set to the current + value. This allows multiple CRC calculations using a common initial + string. + ''' + c = self.new() + c.crcValue = self.crcValue + return c + + def update(self, data): + '''Update the current CRC value using the string specified as the data + parameter. + ''' + self.crcValue = self._crc(data, self.crcValue) + + def digest(self): + '''Return the current CRC value as a string of bytes. The length of + this string is specified in the digest_size attribute. + ''' + n = self.digest_size + crc = self.crcValue + lst = [] + while n > 0: + lst.append(chr(crc & 0xFF)) + crc = crc >> 8 + n -= 1 + lst.reverse() + return ''.join(lst) + + def hexdigest(self): + '''Return the current CRC value as a string of hex digits. The length + of this string is twice the digest_size attribute. + ''' + n = self.digest_size + crc = self.crcValue + lst = [] + while n > 0: + lst.append('%02X' % (crc & 0xFF)) + crc = crc >> 8 + n -= 1 + lst.reverse() + return ''.join(lst) + + def generateCode(self, functionName, out, dataType=None, crcType=None): + '''Generate a C/C++ function. + + functionName -- String specifying the name of the function. + + out -- An open file-like object with a write method. This specifies + where the generated code is written. + + dataType -- An optional parameter specifying the data type of the input + data to the function. Defaults to UINT8. + + crcType -- An optional parameter specifying the data type of the CRC + value. Defaults to one of UINT8, UINT16, UINT32, or UINT64 depending + on the size of the CRC value. + ''' + if dataType is None: + dataType = 'UINT8' + + if crcType is None: + size = 8*self.digest_size + if size == 24: + size = 32 + crcType = 'UINT%d' % size + + if self.digest_size == 1: + # Both 8-bit CRC algorithms are the same + crcAlgor = 'table[*data ^ (%s)crc]' + elif self.reverse: + # The bit reverse algorithms are all the same except for the data + # type of the crc variable which is specified elsewhere. + crcAlgor = 'table[*data ^ (%s)crc] ^ (crc >> 8)' + else: + # The forward CRC algorithms larger than 8 bits have an extra shift + # operation to get the high byte. + shift = 8*(self.digest_size - 1) + crcAlgor = 'table[*data ^ (%%s)(crc >> %d)] ^ (crc << 8)' % shift + + fmt = '0x%%0%dX' % (2*self.digest_size) + if self.digest_size <= 4: + fmt = fmt + 'U,' + else: + # Need the long long type identifier to keep gcc from complaining. + fmt = fmt + 'ULL,' + + # Select the number of entries per row in the output code. + n = {1:8, 2:8, 3:4, 4:4, 8:2}[self.digest_size] + + lst = [] + for i, val in enumerate(self.table): + if (i % n) == 0: + lst.append('\n ') + lst.append(fmt % val) + + poly = 'polynomial: 0x%X' % self.poly + if self.reverse: + poly = poly + ', bit reverse algorithm' + + if self.xorOut: + # Need to remove the comma from the format. + preCondition = '\n crc = crc ^ %s;' % (fmt[:-1] % self.xorOut) + postCondition = preCondition + else: + preCondition = '' + postCondition = '' + + if self.digest_size == 3: + # The 24-bit CRC needs to be conditioned so that only 24-bits are + # used from the 32-bit variable. + if self.reverse: + preCondition += '\n crc = crc & 0xFFFFFFU;' + else: + postCondition += '\n crc = crc & 0xFFFFFFU;' + + + parms = { + 'dataType' : dataType, + 'crcType' : crcType, + 'name' : functionName, + 'crcAlgor' : crcAlgor % dataType, + 'crcTable' : ''.join(lst), + 'poly' : poly, + 'preCondition' : preCondition, + 'postCondition' : postCondition, + } + out.write(_codeTemplate % parms) + +#----------------------------------------------------------------------------- +def mkCrcFun(poly, initCrc=~0L, rev=True, xorOut=0): + '''Return a function that computes the CRC using the specified polynomial. + + poly -- integer representation of the generator polynomial + initCrc -- default initial CRC value + rev -- when true, indicates that the data is processed bit reversed. + xorOut -- the final XOR value + + The returned function has the following user interface + def crcfun(data, crc=initCrc): + ''' + + # First we must verify the params + (sizeBits, initCrc, xorOut) = _verifyParams(poly, initCrc, xorOut) + # Make the function (and table), return the function + return _mkCrcFun(poly, sizeBits, initCrc, rev, xorOut)[0] + +#----------------------------------------------------------------------------- +# Naming convention: +# All function names ending with r are bit reverse variants of the ones +# without the r. + +#----------------------------------------------------------------------------- +# Check the polynomial to make sure that it is acceptable and return the number +# of bits in the CRC. + +def _verifyPoly(poly): + msg = 'The degree of the polynomial must be 8, 16, 24, 32 or 64' + poly = long(poly) # Use a common representation for all operations + for n in (8,16,24,32,64): + low = 1L<> 1 + if ((1L<> 1) ^ poly + else: + crc = crc >> 1 + mask = (1L< 0) + { + crc = %(crcAlgor)s; + data++; + len--; + }%(postCondition)s + return crc; +} +''' + diff -r 4bb2d0975cfe -r a828f320ac58 cutils/crcmod/python2/predefined.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cutils/crcmod/python2/predefined.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,180 @@ +#----------------------------------------------------------------------------- +# Copyright (c) 2010 Craig McQueen +# Copyright (c) 2025 Franz Glasner +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +#----------------------------------------------------------------------------- +''' +crcmod.predefined defines some well-known CRC algorithms. + +To use it, e.g.: + import crcmod.predefined + + crc32func = crcmod.predefined.mkPredefinedCrcFun("crc-32") + crc32class = crcmod.predefined.PredefinedCrc("crc-32") + +crcmod.predefined.Crc is an alias for crcmod.predefined.PredefinedCrc +But if doing 'from crc.predefined import *', only PredefinedCrc is imported. +''' + +from __future__ import absolute_import + +# local imports +from . import crcmod + +__all__ = [ + 'PredefinedCrc', + 'mkPredefinedCrcFun', +] + +REVERSE = True +NON_REVERSE = False + +# +# The following table defines the parameters of well-known CRC algorithms. +# The "Check" value is the CRC for the ASCII byte sequence "123456789". It +# can be used for unit tests. +# +# See also: +# - https://en.wikipedia.org/wiki/Cyclic_redundancy_check +# - https://reveng.sourceforge.io/crc-catalogue/all.htm +# - http://users.ece.cmu.edu/~koopman/crc/index.html +# - https://pycrc.org/models.html +# - https://github.com/marzooqy/anycrc +# +_crc_definitions_table = [ +# Name Identifier-name, Poly Reverse Init-value XOR-out Check + [ 'crc-8', 'Crc8', 0x107, NON_REVERSE, 0x00, 0x00, 0xF4, ], + [ 'crc-8-darc', 'Crc8Darc', 0x139, REVERSE, 0x00, 0x00, 0x15, ], + [ 'crc-8-i-code', 'Crc8ICode', 0x11D, NON_REVERSE, 0xFD, 0x00, 0x7E, ], + [ 'crc-8-itu', 'Crc8Itu', 0x107, NON_REVERSE, 0x55, 0x55, 0xA1, ], + [ 'crc-8-maxim', 'Crc8Maxim', 0x131, REVERSE, 0x00, 0x00, 0xA1, ], + [ 'crc-8-rohc', 'Crc8Rohc', 0x107, REVERSE, 0xFF, 0x00, 0xD0, ], + [ 'crc-8-wcdma', 'Crc8Wcdma', 0x19B, REVERSE, 0x00, 0x00, 0x25, ], + + [ 'crc-16', 'Crc16', 0x18005, REVERSE, 0x0000, 0x0000, 0xBB3D, ], + [ 'crc-16-buypass', 'Crc16Buypass', 0x18005, NON_REVERSE, 0x0000, 0x0000, 0xFEE8, ], + [ 'crc-16-dds-110', 'Crc16Dds110', 0x18005, NON_REVERSE, 0x800D, 0x0000, 0x9ECF, ], + [ 'crc-16-dect', 'Crc16Dect', 0x10589, NON_REVERSE, 0x0001, 0x0001, 0x007E, ], + [ 'crc-16-dnp', 'Crc16Dnp', 0x13D65, REVERSE, 0xFFFF, 0xFFFF, 0xEA82, ], + [ 'crc-16-en-13757', 'Crc16En13757', 0x13D65, NON_REVERSE, 0xFFFF, 0xFFFF, 0xC2B7, ], + [ 'crc-16-genibus', 'Crc16Genibus', 0x11021, NON_REVERSE, 0x0000, 0xFFFF, 0xD64E, ], + [ 'crc-16-maxim', 'Crc16Maxim', 0x18005, REVERSE, 0xFFFF, 0xFFFF, 0x44C2, ], + [ 'crc-16-mcrf4xx', 'Crc16Mcrf4xx', 0x11021, REVERSE, 0xFFFF, 0x0000, 0x6F91, ], + [ 'crc-16-riello', 'Crc16Riello', 0x11021, REVERSE, 0x554D, 0x0000, 0x63D0, ], + [ 'crc-16-t10-dif', 'Crc16T10Dif', 0x18BB7, NON_REVERSE, 0x0000, 0x0000, 0xD0DB, ], + [ 'crc-16-teledisk', 'Crc16Teledisk', 0x1A097, NON_REVERSE, 0x0000, 0x0000, 0x0FB3, ], + [ 'crc-16-usb', 'Crc16Usb', 0x18005, REVERSE, 0x0000, 0xFFFF, 0xB4C8, ], + [ 'x-25', 'CrcX25', 0x11021, REVERSE, 0x0000, 0xFFFF, 0x906E, ], + [ 'xmodem', 'CrcXmodem', 0x11021, NON_REVERSE, 0x0000, 0x0000, 0x31C3, ], + [ 'modbus', 'CrcModbus', 0x18005, REVERSE, 0xFFFF, 0x0000, 0x4B37, ], + + # Note definitions of CCITT are disputable. See: + # http://homepages.tesco.net/~rainstorm/crc-catalogue.htm + # http://web.archive.org/web/20071229021252/http://www.joegeluso.com/software/articles/ccitt.htm + [ 'kermit', 'CrcKermit', 0x11021, REVERSE, 0x0000, 0x0000, 0x2189, ], + [ 'crc-ccitt-false', 'CrcCcittFalse', 0x11021, NON_REVERSE, 0xFFFF, 0x0000, 0x29B1, ], + [ 'crc-aug-ccitt', 'CrcAugCcitt', 0x11021, NON_REVERSE, 0x1D0F, 0x0000, 0xE5CC, ], + + [ 'crc-24', 'Crc24', 0x1864CFB, NON_REVERSE, 0xB704CE, 0x000000, 0x21CF02, ], + [ 'crc-24-flexray-a', 'Crc24FlexrayA', 0x15D6DCB, NON_REVERSE, 0xFEDCBA, 0x000000, 0x7979BD, ], + [ 'crc-24-flexray-b', 'Crc24FlexrayB', 0x15D6DCB, NON_REVERSE, 0xABCDEF, 0x000000, 0x1F23B8, ], + + [ 'crc-32', 'Crc32', 0x104C11DB7, REVERSE, 0x00000000, 0xFFFFFFFF, 0xCBF43926, ], + [ 'crc-32-bzip2', 'Crc32Bzip2', 0x104C11DB7, NON_REVERSE, 0x00000000, 0xFFFFFFFF, 0xFC891918, ], + [ 'crc-32c', 'Crc32C', 0x11EDC6F41, REVERSE, 0x00000000, 0xFFFFFFFF, 0xE3069283, ], + [ 'crc-32d', 'Crc32D', 0x1A833982B, REVERSE, 0x00000000, 0xFFFFFFFF, 0x87315576, ], + [ 'crc-32-mpeg', 'Crc32Mpeg', 0x104C11DB7, NON_REVERSE, 0xFFFFFFFF, 0x00000000, 0x0376E6E7, ], + [ 'posix', 'CrcPosix', 0x104C11DB7, NON_REVERSE, 0xFFFFFFFF, 0xFFFFFFFF, 0x765E7680, ], + [ 'crc-32q', 'Crc32Q', 0x1814141AB, NON_REVERSE, 0x00000000, 0x00000000, 0x3010BF7F, ], + [ 'jamcrc', 'CrcJamCrc', 0x104C11DB7, REVERSE, 0xFFFFFFFF, 0x00000000, 0x340BC6D9, ], + [ 'xfer', 'CrcXfer', 0x1000000AF, NON_REVERSE, 0x00000000, 0x00000000, 0xBD0BE338, ], + +# 64-bit +# Name Identifier-name, Poly Reverse Init-value XOR-out Check + [ 'crc-64', 'Crc64', 0x1000000000000001B, REVERSE, 0x0000000000000000, 0x0000000000000000, 0x46A5A9388A5BEFFE, ], # ISO POLY + # See https://lwn.net/Articles/976030/ (MCRC64, used as CRC-64 in the Linux kernel) + [ 'crc-64-2', 'Crc64_2', 0x1000000000000001B, NON_REVERSE, 0x0000000000000000, 0x0000000000000000, 0xE4FFBEA588933790, ], # fag, ISO POLY + [ 'crc-64-go', 'Crc64Go', 0x1000000000000001B, REVERSE, 0x0000000000000000, 0xFFFFFFFFFFFFFFFF, 0xB90956C775A41001, ], # fag, ISO POLY + [ 'crc-64-ecma', 'Crc64Ecma', 0x142F0E1EBA9EA3693, NON_REVERSE, 0x0000000000000000, 0x0000000000000000, 0x6C40DF5F0B497347, ], # fag + [ 'crc-64-we', 'Crc64We', 0x142F0E1EBA9EA3693, NON_REVERSE, 0x0000000000000000, 0xFFFFFFFFFFFFFFFF, 0x62EC59E3F1A4F00A, ], + [ 'crc-64-jones', 'Crc64Jones', 0x1AD93D23594C935A9, REVERSE, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000, 0xCAA717168609F281, ], + [ 'crc-64-xz', 'Crc64Xz', 0x142F0E1EBA9EA3693, REVERSE, 0x0000000000000000, 0xFFFFFFFFFFFFFFFF, 0x995DC9BBDF1939FA, ], # fag, ECMA POLY + [ 'crc-64-redis', 'Crc64Redis', 0x1AD93D23594C935A9, REVERSE, 0x0000000000000000, 0x0000000000000000, 0xE9C6D914C4b8D9CA, ], # fag +] + + +def _simplify_name(name): + """ + Reduce CRC definition name to a simplified form: + * lowercase + * dashes removed + * spaces removed + * any initial "CRC" string removed + """ + name = name.lower() + name = name.replace('-', '') + name = name.replace(' ', '') + if name.startswith('crc'): + name = name[len('crc'):] + return name + + +_crc_definitions_by_name = {} +_crc_definitions_by_identifier = {} +_crc_definitions = [] + +_crc_table_headings = [ 'name', 'identifier', 'poly', 'reverse', 'init', 'xor_out', 'check' ] + +for table_entry in _crc_definitions_table: + crc_definition = dict(zip(_crc_table_headings, table_entry)) + _crc_definitions.append(crc_definition) + name = _simplify_name(table_entry[0]) + if name in _crc_definitions_by_name: + raise Exception("Duplicate entry for '%s' in CRC table" % name) + _crc_definitions_by_name[name] = crc_definition + _crc_definitions_by_identifier[table_entry[1]] = crc_definition + + +def _get_definition_by_name(crc_name): + definition = _crc_definitions_by_name.get(_simplify_name(crc_name), None) + if not definition: + definition = _crc_definitions_by_identifier.get(crc_name, None) + if not definition: + raise KeyError("Unkown CRC name '%s'" % crc_name) + return definition + + +class PredefinedCrc(crcmod.Crc): + def __init__(self, crc_name): + definition = _get_definition_by_name(crc_name) + crcmod.Crc.__init__(self, poly=definition['poly'], initCrc=definition['init'], rev=definition['reverse'], xorOut=definition['xor_out']) + + +# crcmod.predefined.Crc is an alias for crcmod.predefined.PredefinedCrc +Crc = PredefinedCrc + + +def mkPredefinedCrcFun(crc_name): + definition = _get_definition_by_name(crc_name) + return crcmod.mkCrcFun(poly=definition['poly'], initCrc=definition['init'], rev=definition['reverse'], xorOut=definition['xor_out']) + + +# crcmod.predefined.mkCrcFun is an alias for crcmod.predefined.mkPredefinedCrcFun +mkCrcFun = mkPredefinedCrcFun diff -r 4bb2d0975cfe -r a828f320ac58 cutils/crcmod/python2/test.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cutils/crcmod/python2/test.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,496 @@ +#----------------------------------------------------------------------------- +# Copyright (c) 2010 Raymond L. Buvel +# Copyright (c) 2010 Craig McQueen +# Copyright (c) 2025 Franz Glasner +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +#----------------------------------------------------------------------------- +'''Unit tests for crcmod functionality''' + + +from __future__ import absolute_import + +import unittest +import binascii + +from .crcmod import mkCrcFun, Crc +from .crcmod import _usingExtension +from .predefined import PredefinedCrc +from .predefined import mkPredefinedCrcFun +from .predefined import _crc_definitions as _predefined_crc_definitions + + +#----------------------------------------------------------------------------- +# This polynomial was chosen because it is the product of two irreducible +# polynomials. +# g8 = (x^7+x+1)*(x+1) +g8 = 0x185 + +#----------------------------------------------------------------------------- +# The following reproduces all of the entries in the Numerical Recipes table. +# This is the standard CCITT polynomial. +g16 = 0x11021 + +#----------------------------------------------------------------------------- +g24 = 0x15D6DCB + +#----------------------------------------------------------------------------- +# This is the standard AUTODIN-II polynomial which appears to be used in a +# wide variety of standards and applications. +g32 = 0x104C11DB7 + + +#----------------------------------------------------------------------------- +# I was able to locate a couple of 64-bit polynomials on the web. To make it +# easier to input the representation, define a function that builds a +# polynomial from a list of the bits that need to be turned on. + +def polyFromBits(bits): + p = 0L + for n in bits: + p = p | (1L << n) + return p + +# The following is from the paper "An Improved 64-bit Cyclic Redundancy Check +# for Protein Sequences" by David T. Jones + +g64a = polyFromBits([64, 63, 61, 59, 58, 56, 55, 52, 49, 48, 47, 46, 44, 41, + 37, 36, 34, 32, 31, 28, 26, 23, 22, 19, 16, 13, 12, 10, 9, 6, 4, + 3, 0]) + +# The following is from Standard ECMA-182 "Data Interchange on 12,7 mm 48-Track +# Magnetic Tape Cartridges -DLT1 Format-", December 1992. + +g64b = polyFromBits([64, 62, 57, 55, 54, 53, 52, 47, 46, 45, 40, 39, 38, 37, + 35, 33, 32, 31, 29, 27, 24, 23, 22, 21, 19, 17, 13, 12, 10, 9, 7, + 4, 1, 0]) + +#----------------------------------------------------------------------------- +# This class is used to check the CRC calculations against a direct +# implementation using polynomial division. + +class poly: + '''Class implementing polynomials over the field of integers mod 2''' + def __init__(self,p): + p = long(p) + if p < 0: raise ValueError('invalid polynomial') + self.p = p + + def __long__(self): + return self.p + + def __eq__(self,other): + return self.p == other.p + + def __ne__(self,other): + return self.p != other.p + + # To allow sorting of polynomials, use their long integer form for + # comparison + def __cmp__(self,other): + return cmp(self.p, other.p) + + def __nonzero__(self): + return self.p != 0L + + def __neg__(self): + return self # These polynomials are their own inverse under addition + + def __invert__(self): + n = max(self.deg() + 1, 1) + x = (1L << n) - 1 + return poly(self.p ^ x) + + def __add__(self,other): + return poly(self.p ^ other.p) + + def __sub__(self,other): + return poly(self.p ^ other.p) + + def __mul__(self,other): + a = self.p + b = other.p + if a == 0 or b == 0: return poly(0) + x = 0L + while b: + if b&1: + x = x ^ a + a = a<<1 + b = b>>1 + return poly(x) + + def __divmod__(self,other): + u = self.p + m = self.deg() + v = other.p + n = other.deg() + if v == 0: raise ZeroDivisionError('polynomial division by zero') + if n == 0: return (self,poly(0)) + if m < n: return (poly(0),self) + k = m-n + a = 1L << m + v = v << k + q = 0L + while k > 0: + if a & u: + u = u ^ v + q = q | 1L + q = q << 1 + a = a >> 1 + v = v >> 1 + k -= 1 + if a & u: + u = u ^ v + q = q | 1L + return (poly(q),poly(u)) + + def __div__(self,other): + return self.__divmod__(other)[0] + + def __mod__(self,other): + return self.__divmod__(other)[1] + + def __repr__(self): + return 'poly(0x%XL)' % self.p + + def __str__(self): + p = self.p + if p == 0: return '0' + lst = { 0:[], 1:['1'], 2:['x'], 3:['1','x'] }[p&3] + p = p>>2 + n = 2 + while p: + if p&1: lst.append('x^%d' % n) + p = p>>1 + n += 1 + lst.reverse() + return '+'.join(lst) + + def deg(self): + '''return the degree of the polynomial''' + a = self.p + if a == 0: return -1 + n = 0 + while a >= 0x10000L: + n += 16 + a = a >> 16 + a = int(a) + while a > 1: + n += 1 + a = a >> 1 + return n + +#----------------------------------------------------------------------------- +# The following functions compute the CRC using direct polynomial division. +# These functions are checked against the result of the table driven +# algorithms. + +g8p = poly(g8) +x8p = poly(1L<<8) +def crc8p(d): + d = map(ord, d) + p = 0L + for i in d: + p = p*256L + i + p = poly(p) + return long(p*x8p%g8p) + +g16p = poly(g16) +x16p = poly(1L<<16) +def crc16p(d): + d = map(ord, d) + p = 0L + for i in d: + p = p*256L + i + p = poly(p) + return long(p*x16p%g16p) + +g24p = poly(g24) +x24p = poly(1L<<24) +def crc24p(d): + d = map(ord, d) + p = 0L + for i in d: + p = p*256L + i + p = poly(p) + return long(p*x24p%g24p) + +g32p = poly(g32) +x32p = poly(1L<<32) +def crc32p(d): + d = map(ord, d) + p = 0L + for i in d: + p = p*256L + i + p = poly(p) + return long(p*x32p%g32p) + +g64ap = poly(g64a) +x64p = poly(1L<<64) +def crc64ap(d): + d = map(ord, d) + p = 0L + for i in d: + p = p*256L + i + p = poly(p) + return long(p*x64p%g64ap) + +g64bp = poly(g64b) +def crc64bp(d): + d = map(ord, d) + p = 0L + for i in d: + p = p*256L + i + p = poly(p) + return long(p*x64p%g64bp) + + +class KnownAnswerTests(unittest.TestCase): + test_messages = [ + 'T', + 'CatMouse987654321', + ] + + known_answers = [ + [ (g8,0,0), (0xFE, 0x9D) ], + [ (g8,-1,1), (0x4F, 0x9B) ], + [ (g8,0,1), (0xFE, 0x62) ], + [ (g16,0,0), (0x1A71, 0xE556) ], + [ (g16,-1,1), (0x1B26, 0xF56E) ], + [ (g16,0,1), (0x14A1, 0xC28D) ], + [ (g24,0,0), (0xBCC49D, 0xC4B507) ], + [ (g24,-1,1), (0x59BD0E, 0x0AAA37) ], + [ (g24,0,1), (0xD52B0F, 0x1523AB) ], + [ (g32,0,0), (0x6B93DDDB, 0x12DCA0F4) ], + [ (g32,0xFFFFFFFFL,1), (0x41FB859FL, 0xF7B400A7L) ], + [ (g32,0,1), (0x6C0695EDL, 0xC1A40EE5L) ], + [ (g32,0,1,0xFFFFFFFF), (0xBE047A60L, 0x084BFF58L) ], + ] + + def test_known_answers(self): + for crcfun_params, v in self.known_answers: + crcfun = mkCrcFun(*crcfun_params) + self.assertEqual(crcfun('',0), 0, "Wrong answer for CRC parameters %s, input ''" % (crcfun_params,)) + for i, msg in enumerate(self.test_messages): + self.assertEqual(crcfun(msg), v[i], "Wrong answer for CRC parameters %s, input '%s'" % (crcfun_params,msg)) + self.assertEqual(crcfun(msg[4:], crcfun(msg[:4])), v[i], "Wrong answer for CRC parameters %s, input '%s'" % (crcfun_params,msg)) + self.assertEqual(crcfun(msg[-1:], crcfun(msg[:-1])), v[i], "Wrong answer for CRC parameters %s, input '%s'" % (crcfun_params,msg)) + + +class CompareReferenceCrcTest(unittest.TestCase): + test_messages = [ + '', + 'T', + '123456789', + 'CatMouse987654321', + ] + + test_poly_crcs = [ + [ (g8,0,0), crc8p ], + [ (g16,0,0), crc16p ], + [ (g24,0,0), crc24p ], + [ (g32,0,0), crc32p ], + [ (g64a,0,0), crc64ap ], + [ (g64b,0,0), crc64bp ], + ] + + @staticmethod + def reference_crc32(d, crc=0): + """This function modifies the return value of binascii.crc32 + to be an unsigned 32-bit value. I.e. in the range 0 to 2**32-1.""" + # Work around the future warning on constants. + if crc > 0x7FFFFFFFL: + x = int(crc & 0x7FFFFFFFL) + crc = x | -2147483648 + x = binascii.crc32(d,crc) + return long(x) & 0xFFFFFFFFL + + def test_compare_crc32(self): + """The binascii module has a 32-bit CRC function that is used in a wide range + of applications including the checksum used in the ZIP file format. + This test compares the CRC-32 implementation of this crcmod module to + that of binascii.crc32.""" + # The following function should produce the same result as + # self.reference_crc32 which is derived from binascii.crc32. + crc32 = mkCrcFun(g32,0,1,0xFFFFFFFF) + + for msg in self.test_messages: + self.assertEqual(crc32(msg), self.reference_crc32(msg)) + + def test_compare_poly(self): + """Compare various CRCs of this crcmod module to a pure + polynomial-based implementation.""" + for crcfun_params, crc_poly_fun in self.test_poly_crcs: + # The following function should produce the same result as + # the associated polynomial CRC function. + crcfun = mkCrcFun(*crcfun_params) + + for msg in self.test_messages: + self.assertEqual(crcfun(msg), crc_poly_fun(msg)) + + +class CrcClassTest(unittest.TestCase): + """Verify the Crc class""" + + msg = 'CatMouse987654321' + + def test_simple_crc32_class(self): + """Verify the CRC class when not using xorOut""" + crc = Crc(g32) + + str_rep = \ +'''poly = 0x104C11DB7 +reverse = True +initCrc = 0xFFFFFFFF +xorOut = 0x00000000 +crcValue = 0xFFFFFFFF''' + self.assertEqual(str(crc), str_rep) + self.assertEqual(crc.digest(), '\xff\xff\xff\xff') + self.assertEqual(crc.hexdigest(), 'FFFFFFFF') + + crc.update(self.msg) + self.assertEqual(crc.crcValue, 0xF7B400A7L) + self.assertEqual(crc.digest(), '\xf7\xb4\x00\xa7') + self.assertEqual(crc.hexdigest(), 'F7B400A7') + + # Verify the .copy() method + x = crc.copy() + self.assertTrue(x is not crc) + str_rep = \ +'''poly = 0x104C11DB7 +reverse = True +initCrc = 0xFFFFFFFF +xorOut = 0x00000000 +crcValue = 0xF7B400A7''' + self.assertEqual(str(crc), str_rep) + self.assertEqual(str(x), str_rep) + + def test_full_crc32_class(self): + """Verify the CRC class when using xorOut""" + + crc = Crc(g32, initCrc=0, xorOut= ~0L) + + str_rep = \ +'''poly = 0x104C11DB7 +reverse = True +initCrc = 0x00000000 +xorOut = 0xFFFFFFFF +crcValue = 0x00000000''' + self.assertEqual(str(crc), str_rep) + self.assertEqual(crc.digest(), '\x00\x00\x00\x00') + self.assertEqual(crc.hexdigest(), '00000000') + + crc.update(self.msg) + self.assertEqual(crc.crcValue, 0x84BFF58L) + self.assertEqual(crc.digest(), '\x08\x4b\xff\x58') + self.assertEqual(crc.hexdigest(), '084BFF58') + + # Verify the .copy() method + x = crc.copy() + self.assertTrue(x is not crc) + str_rep = \ +'''poly = 0x104C11DB7 +reverse = True +initCrc = 0x00000000 +xorOut = 0xFFFFFFFF +crcValue = 0x084BFF58''' + self.assertEqual(str(crc), str_rep) + self.assertEqual(str(x), str_rep) + + # Verify the .new() method + y = crc.new() + self.assertTrue(y is not crc) + self.assertTrue(y is not x) + str_rep = \ +'''poly = 0x104C11DB7 +reverse = True +initCrc = 0x00000000 +xorOut = 0xFFFFFFFF +crcValue = 0x00000000''' + self.assertEqual(str(y), str_rep) + + +class PredefinedCrcTest(unittest.TestCase): + """Verify the predefined CRCs""" + + test_messages_for_known_answers = [ + '', # Test cases below depend on this first entry being the empty string. + 'T', + 'CatMouse987654321', + ] + + known_answers = [ + [ 'crc-aug-ccitt', (0x1D0F, 0xD6ED, 0x5637) ], + [ 'x-25', (0x0000, 0xE4D9, 0x0A91) ], + [ 'crc-32', (0x00000000, 0xBE047A60, 0x084BFF58) ], + ] + + def test_known_answers(self): + for crcfun_name, v in self.known_answers: + crcfun = mkPredefinedCrcFun(crcfun_name) + self.assertEqual(crcfun('',0), 0, "Wrong answer for CRC '%s', input ''" % crcfun_name) + for i, msg in enumerate(self.test_messages_for_known_answers): + self.assertEqual(crcfun(msg), v[i], "Wrong answer for CRC %s, input '%s'" % (crcfun_name,msg)) + self.assertEqual(crcfun(msg[4:], crcfun(msg[:4])), v[i], "Wrong answer for CRC %s, input '%s'" % (crcfun_name,msg)) + self.assertEqual(crcfun(msg[-1:], crcfun(msg[:-1])), v[i], "Wrong answer for CRC %s, input '%s'" % (crcfun_name,msg)) + + def test_class_with_known_answers(self): + for crcfun_name, v in self.known_answers: + for i, msg in enumerate(self.test_messages_for_known_answers): + crc1 = PredefinedCrc(crcfun_name) + crc1.update(msg) + self.assertEqual(crc1.crcValue, v[i], "Wrong answer for crc1 %s, input '%s'" % (crcfun_name,msg)) + + crc2 = crc1.new() + # Check that crc1 maintains its same value, after .new() call. + self.assertEqual(crc1.crcValue, v[i], "Wrong state for crc1 %s, input '%s'" % (crcfun_name,msg)) + # Check that the new class instance created by .new() contains the initialisation value. + # This depends on the first string in self.test_messages_for_known_answers being + # the empty string. + self.assertEqual(crc2.crcValue, v[0], "Wrong state for crc2 %s, input '%s'" % (crcfun_name,msg)) + + crc2.update(msg) + # Check that crc1 maintains its same value, after crc2 has called .update() + self.assertEqual(crc1.crcValue, v[i], "Wrong state for crc1 %s, input '%s'" % (crcfun_name,msg)) + # Check that crc2 contains the right value after calling .update() + self.assertEqual(crc2.crcValue, v[i], "Wrong state for crc2 %s, input '%s'" % (crcfun_name,msg)) + + def test_function_predefined_table(self): + for table_entry in _predefined_crc_definitions: + # Check predefined function + crc_func = mkPredefinedCrcFun(table_entry['name']) + calc_value = crc_func("123456789") + self.assertEqual(calc_value, table_entry['check'], "Wrong answer for CRC '%s'" % table_entry['name']) + + def test_class_predefined_table(self): + for table_entry in _predefined_crc_definitions: + # Check predefined class + crc1 = PredefinedCrc(table_entry['name']) + crc1.update("123456789") + self.assertEqual(crc1.crcValue, table_entry['check'], "Wrong answer for CRC '%s'" % table_entry['name']) + + +def runtests(): + print "Using extension:", _usingExtension + print + unittest.main() + + +if __name__ == '__main__': + runtests() diff -r 4bb2d0975cfe -r a828f320ac58 cutils/crcmod/python3/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cutils/crcmod/python3/__init__.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +#----------------------------------------------------------------------------- +# Copyright (c) 2010 Raymond L. Buvel +# Copyright (c) 2010 Craig McQueen +# Copyright (c) 2025 Franz Glasner +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +#----------------------------------------------------------------------------- diff -r 4bb2d0975cfe -r a828f320ac58 cutils/crcmod/python3/_crcfunpy.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cutils/crcmod/python3/_crcfunpy.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,107 @@ +#----------------------------------------------------------------------------- +# Low level CRC functions for use by crcmod. This version is implemented in +# Python for a couple of reasons. 1) Provide a reference implememtation. +# 2) Provide a version that can be used on systems where a C compiler is not +# available for building extension modules. +# +# Copyright (c) 2009 Raymond L. Buvel +# Copyright (c) 2010 Craig McQueen +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +#----------------------------------------------------------------------------- + +def _get_buffer_view(in_obj): + if isinstance(in_obj, str): + raise TypeError('Unicode-objects must be encoded before calculating a CRC') + mv = memoryview(in_obj) + if mv.ndim > 1: + raise BufferError('Buffer must be single dimension') + return mv + + +def _crc8(data, crc, table): + mv = _get_buffer_view(data) + crc = crc & 0xFF + for x in mv.tobytes(): + crc = table[x ^ crc] + return crc + +def _crc8r(data, crc, table): + mv = _get_buffer_view(data) + crc = crc & 0xFF + for x in mv.tobytes(): + crc = table[x ^ crc] + return crc + +def _crc16(data, crc, table): + mv = _get_buffer_view(data) + crc = crc & 0xFFFF + for x in mv.tobytes(): + crc = table[x ^ ((crc>>8) & 0xFF)] ^ ((crc << 8) & 0xFF00) + return crc + +def _crc16r(data, crc, table): + mv = _get_buffer_view(data) + crc = crc & 0xFFFF + for x in mv.tobytes(): + crc = table[x ^ (crc & 0xFF)] ^ (crc >> 8) + return crc + +def _crc24(data, crc, table): + mv = _get_buffer_view(data) + crc = crc & 0xFFFFFF + for x in mv.tobytes(): + crc = table[x ^ (crc>>16 & 0xFF)] ^ ((crc << 8) & 0xFFFF00) + return crc + +def _crc24r(data, crc, table): + mv = _get_buffer_view(data) + crc = crc & 0xFFFFFF + for x in mv.tobytes(): + crc = table[x ^ (crc & 0xFF)] ^ (crc >> 8) + return crc + +def _crc32(data, crc, table): + mv = _get_buffer_view(data) + crc = crc & 0xFFFFFFFF + for x in mv.tobytes(): + crc = table[x ^ ((crc>>24) & 0xFF)] ^ ((crc << 8) & 0xFFFFFF00) + return crc + +def _crc32r(data, crc, table): + mv = _get_buffer_view(data) + crc = crc & 0xFFFFFFFF + for x in mv.tobytes(): + crc = table[x ^ (crc & 0xFF)] ^ (crc >> 8) + return crc + +def _crc64(data, crc, table): + mv = _get_buffer_view(data) + crc = crc & 0xFFFFFFFFFFFFFFFF + for x in mv.tobytes(): + crc = table[x ^ ((crc>>56) & 0xFF)] ^ ((crc << 8) & 0xFFFFFFFFFFFFFF00) + return crc + +def _crc64r(data, crc, table): + mv = _get_buffer_view(data) + crc = crc & 0xFFFFFFFFFFFFFFFF + for x in mv.tobytes(): + crc = table[x ^ (crc & 0xFF)] ^ (crc >> 8) + return crc + diff -r 4bb2d0975cfe -r a828f320ac58 cutils/crcmod/python3/crcmod.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cutils/crcmod/python3/crcmod.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,457 @@ +#----------------------------------------------------------------------------- +# Copyright (c) 2010 Raymond L. Buvel +# Copyright (c) 2010 Craig McQueen +# Copyright (c) 2025 Franz Glasner +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +#----------------------------------------------------------------------------- +'''crcmod is a Python module for gererating objects that compute the Cyclic +Redundancy Check. Any 8, 16, 24, 32, or 64 bit polynomial can be used. + +The following are the public components of this module. + +Crc -- a class that creates instances providing the same interface as the +algorithms in the hashlib module in the Python standard library. These +instances also provide a method for generating a C/C++ function to compute +the CRC. + +mkCrcFun -- create a Python function to compute the CRC using the specified +polynomial and initial value. This provides a much simpler interface if +all you need is a function for CRC calculation. +''' + +__all__ = '''mkCrcFun Crc'''.split() + +# Select the appropriate set of low-level CRC functions for this installation. +# If the extension module was not built, drop back to the Python implementation +# even though it is significantly slower. +try: + from . import _crcfunext as _crcfun + _usingExtension = True +except ImportError: + from . import _crcfunpy as _crcfun + _usingExtension = False + +import sys, struct + +#----------------------------------------------------------------------------- +class Crc: + '''Compute a Cyclic Redundancy Check (CRC) using the specified polynomial. + + Instances of this class have the same interface as the algorithms in the + hashlib module in the Python standard library. See the documentation of + this module for examples of how to use a Crc instance. + + The string representation of a Crc instance identifies the polynomial, + initial value, XOR out value, and the current CRC value. The print + statement can be used to output this information. + + If you need to generate a C/C++ function for use in another application, + use the generateCode method. If you need to generate code for another + language, subclass Crc and override the generateCode method. + + The following are the parameters supplied to the constructor. + + poly -- The generator polynomial to use in calculating the CRC. The value + is specified as a Python integer. The bits in this integer are the + coefficients of the polynomial. The only polynomials allowed are those + that generate 8, 16, 24, 32, or 64 bit CRCs. + + initCrc -- Initial value used to start the CRC calculation. This initial + value should be the initial shift register value XORed with the final XOR + value. That is equivalent to the CRC result the algorithm should return for + a zero-length string. Defaults to all bits set because that starting value + will take leading zero bytes into account. Starting with zero will ignore + all leading zero bytes. + + rev -- A flag that selects a bit reversed algorithm when True. Defaults to + True because the bit reversed algorithms are more efficient. + + xorOut -- Final value to XOR with the calculated CRC value. Used by some + CRC algorithms. Defaults to zero. + ''' + def __init__(self, poly, initCrc=~0, rev=True, xorOut=0, initialize=True): + if not initialize: + # Don't want to perform the initialization when using new or copy + # to create a new instance. + return + + (sizeBits, initCrc, xorOut) = _verifyParams(poly, initCrc, xorOut) + self.digest_size = sizeBits//8 + self.initCrc = initCrc + self.xorOut = xorOut + + self.poly = poly + self.reverse = rev + + (crcfun, table) = _mkCrcFun(poly, sizeBits, initCrc, rev, xorOut) + self._crc = crcfun + self.table = table + + self.crcValue = self.initCrc + + def __str__(self): + lst = [] + lst.append('poly = 0x%X' % self.poly) + lst.append('reverse = %s' % self.reverse) + fmt = '0x%%0%dX' % (self.digest_size*2) + lst.append('initCrc = %s' % (fmt % self.initCrc)) + lst.append('xorOut = %s' % (fmt % self.xorOut)) + lst.append('crcValue = %s' % (fmt % self.crcValue)) + return '\n'.join(lst) + + def new(self, arg=None): + '''Create a new instance of the Crc class initialized to the same + values as the original instance. The current CRC is set to the initial + value. If a string is provided in the optional arg parameter, it is + passed to the update method. + ''' + n = Crc(poly=None, initialize=False) + n._crc = self._crc + n.digest_size = self.digest_size + n.initCrc = self.initCrc + n.xorOut = self.xorOut + n.table = self.table + n.crcValue = self.initCrc + n.reverse = self.reverse + n.poly = self.poly + if arg is not None: + n.update(arg) + return n + + def copy(self): + '''Create a new instance of the Crc class initialized to the same + values as the original instance. The current CRC is set to the current + value. This allows multiple CRC calculations using a common initial + string. + ''' + c = self.new() + c.crcValue = self.crcValue + return c + + def update(self, data): + '''Update the current CRC value using the string specified as the data + parameter. + ''' + self.crcValue = self._crc(data, self.crcValue) + + def digest(self): + '''Return the current CRC value as a string of bytes. The length of + this string is specified in the digest_size attribute. + ''' + n = self.digest_size + crc = self.crcValue + lst = [] + while n > 0: + lst.append(crc & 0xFF) + crc = crc >> 8 + n -= 1 + lst.reverse() + return bytes(lst) + + def hexdigest(self): + '''Return the current CRC value as a string of hex digits. The length + of this string is twice the digest_size attribute. + ''' + n = self.digest_size + crc = self.crcValue + lst = [] + while n > 0: + lst.append('%02X' % (crc & 0xFF)) + crc = crc >> 8 + n -= 1 + lst.reverse() + return ''.join(lst) + + def generateCode(self, functionName, out, dataType=None, crcType=None): + '''Generate a C/C++ function. + + functionName -- String specifying the name of the function. + + out -- An open file-like object with a write method. This specifies + where the generated code is written. + + dataType -- An optional parameter specifying the data type of the input + data to the function. Defaults to UINT8. + + crcType -- An optional parameter specifying the data type of the CRC + value. Defaults to one of UINT8, UINT16, UINT32, or UINT64 depending + on the size of the CRC value. + ''' + if dataType is None: + dataType = 'UINT8' + + if crcType is None: + size = 8*self.digest_size + if size == 24: + size = 32 + crcType = 'UINT%d' % size + + if self.digest_size == 1: + # Both 8-bit CRC algorithms are the same + crcAlgor = 'table[*data ^ (%s)crc]' + elif self.reverse: + # The bit reverse algorithms are all the same except for the data + # type of the crc variable which is specified elsewhere. + crcAlgor = 'table[*data ^ (%s)crc] ^ (crc >> 8)' + else: + # The forward CRC algorithms larger than 8 bits have an extra shift + # operation to get the high byte. + shift = 8*(self.digest_size - 1) + crcAlgor = 'table[*data ^ (%%s)(crc >> %d)] ^ (crc << 8)' % shift + + fmt = '0x%%0%dX' % (2*self.digest_size) + if self.digest_size <= 4: + fmt = fmt + 'U,' + else: + # Need the long long type identifier to keep gcc from complaining. + fmt = fmt + 'ULL,' + + # Select the number of entries per row in the output code. + n = {1:8, 2:8, 3:4, 4:4, 8:2}[self.digest_size] + + lst = [] + for i, val in enumerate(self.table): + if (i % n) == 0: + lst.append('\n ') + lst.append(fmt % val) + + poly = 'polynomial: 0x%X' % self.poly + if self.reverse: + poly = poly + ', bit reverse algorithm' + + if self.xorOut: + # Need to remove the comma from the format. + preCondition = '\n crc = crc ^ %s;' % (fmt[:-1] % self.xorOut) + postCondition = preCondition + else: + preCondition = '' + postCondition = '' + + if self.digest_size == 3: + # The 24-bit CRC needs to be conditioned so that only 24-bits are + # used from the 32-bit variable. + if self.reverse: + preCondition += '\n crc = crc & 0xFFFFFFU;' + else: + postCondition += '\n crc = crc & 0xFFFFFFU;' + + + parms = { + 'dataType' : dataType, + 'crcType' : crcType, + 'name' : functionName, + 'crcAlgor' : crcAlgor % dataType, + 'crcTable' : ''.join(lst), + 'poly' : poly, + 'preCondition' : preCondition, + 'postCondition' : postCondition, + } + out.write(_codeTemplate % parms) + +#----------------------------------------------------------------------------- +def mkCrcFun(poly, initCrc=~0, rev=True, xorOut=0): + '''Return a function that computes the CRC using the specified polynomial. + + poly -- integer representation of the generator polynomial + initCrc -- default initial CRC value + rev -- when true, indicates that the data is processed bit reversed. + xorOut -- the final XOR value + + The returned function has the following user interface + def crcfun(data, crc=initCrc): + ''' + + # First we must verify the params + (sizeBits, initCrc, xorOut) = _verifyParams(poly, initCrc, xorOut) + # Make the function (and table), return the function + return _mkCrcFun(poly, sizeBits, initCrc, rev, xorOut)[0] + +#----------------------------------------------------------------------------- +# Naming convention: +# All function names ending with r are bit reverse variants of the ones +# without the r. + +#----------------------------------------------------------------------------- +# Check the polynomial to make sure that it is acceptable and return the number +# of bits in the CRC. + +def _verifyPoly(poly): + msg = 'The degree of the polynomial must be 8, 16, 24, 32 or 64' + for n in (8,16,24,32,64): + low = 1<> 1 + return y + +#----------------------------------------------------------------------------- +# The following functions compute the CRC for a single byte. These are used +# to build up the tables needed in the CRC algorithm. Assumes the high order +# bit of the polynomial has been stripped off. + +def _bytecrc(crc, poly, n): + mask = 1<<(n-1) + for i in range(8): + if crc & mask: + crc = (crc << 1) ^ poly + else: + crc = crc << 1 + mask = (1<> 1) ^ poly + else: + crc = crc >> 1 + mask = (1< 0) + { + crc = %(crcAlgor)s; + data++; + len--; + }%(postCondition)s + return crc; +} +''' + diff -r 4bb2d0975cfe -r a828f320ac58 cutils/crcmod/python3/predefined.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cutils/crcmod/python3/predefined.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,178 @@ +#----------------------------------------------------------------------------- +# Copyright (c) 2010 Craig McQueen +# Copyright (c) 2025 Franz Glasner +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +#----------------------------------------------------------------------------- +''' +crcmod.predefined defines some well-known CRC algorithms. + +To use it, e.g.: + import crcmod.predefined + + crc32func = crcmod.predefined.mkPredefinedCrcFun("crc-32") + crc32class = crcmod.predefined.PredefinedCrc("crc-32") + +crcmod.predefined.Crc is an alias for crcmod.predefined.PredefinedCrc +But if doing 'from crc.predefined import *', only PredefinedCrc is imported. +''' + +# local imports +from . import crcmod + +__all__ = [ + 'PredefinedCrc', + 'mkPredefinedCrcFun', +] + +REVERSE = True +NON_REVERSE = False + +# +# The following table defines the parameters of well-known CRC algorithms. +# The "Check" value is the CRC for the ASCII byte sequence b"123456789". It +# can be used for unit tests. +# +# See also: +# - https://en.wikipedia.org/wiki/Cyclic_redundancy_check +# - https://reveng.sourceforge.io/crc-catalogue/all.htm +# - http://users.ece.cmu.edu/~koopman/crc/index.html +# - https://pycrc.org/models.html +# - https://github.com/marzooqy/anycrc +# +_crc_definitions_table = [ +# Name Identifier-name, Poly Reverse Init-value XOR-out Check + [ 'crc-8', 'Crc8', 0x107, NON_REVERSE, 0x00, 0x00, 0xF4, ], + [ 'crc-8-darc', 'Crc8Darc', 0x139, REVERSE, 0x00, 0x00, 0x15, ], + [ 'crc-8-i-code', 'Crc8ICode', 0x11D, NON_REVERSE, 0xFD, 0x00, 0x7E, ], + [ 'crc-8-itu', 'Crc8Itu', 0x107, NON_REVERSE, 0x55, 0x55, 0xA1, ], + [ 'crc-8-maxim', 'Crc8Maxim', 0x131, REVERSE, 0x00, 0x00, 0xA1, ], + [ 'crc-8-rohc', 'Crc8Rohc', 0x107, REVERSE, 0xFF, 0x00, 0xD0, ], + [ 'crc-8-wcdma', 'Crc8Wcdma', 0x19B, REVERSE, 0x00, 0x00, 0x25, ], + + [ 'crc-16', 'Crc16', 0x18005, REVERSE, 0x0000, 0x0000, 0xBB3D, ], + [ 'crc-16-buypass', 'Crc16Buypass', 0x18005, NON_REVERSE, 0x0000, 0x0000, 0xFEE8, ], + [ 'crc-16-dds-110', 'Crc16Dds110', 0x18005, NON_REVERSE, 0x800D, 0x0000, 0x9ECF, ], + [ 'crc-16-dect', 'Crc16Dect', 0x10589, NON_REVERSE, 0x0001, 0x0001, 0x007E, ], + [ 'crc-16-dnp', 'Crc16Dnp', 0x13D65, REVERSE, 0xFFFF, 0xFFFF, 0xEA82, ], + [ 'crc-16-en-13757', 'Crc16En13757', 0x13D65, NON_REVERSE, 0xFFFF, 0xFFFF, 0xC2B7, ], + [ 'crc-16-genibus', 'Crc16Genibus', 0x11021, NON_REVERSE, 0x0000, 0xFFFF, 0xD64E, ], + [ 'crc-16-maxim', 'Crc16Maxim', 0x18005, REVERSE, 0xFFFF, 0xFFFF, 0x44C2, ], + [ 'crc-16-mcrf4xx', 'Crc16Mcrf4xx', 0x11021, REVERSE, 0xFFFF, 0x0000, 0x6F91, ], + [ 'crc-16-riello', 'Crc16Riello', 0x11021, REVERSE, 0x554D, 0x0000, 0x63D0, ], + [ 'crc-16-t10-dif', 'Crc16T10Dif', 0x18BB7, NON_REVERSE, 0x0000, 0x0000, 0xD0DB, ], + [ 'crc-16-teledisk', 'Crc16Teledisk', 0x1A097, NON_REVERSE, 0x0000, 0x0000, 0x0FB3, ], + [ 'crc-16-usb', 'Crc16Usb', 0x18005, REVERSE, 0x0000, 0xFFFF, 0xB4C8, ], + [ 'x-25', 'CrcX25', 0x11021, REVERSE, 0x0000, 0xFFFF, 0x906E, ], + [ 'xmodem', 'CrcXmodem', 0x11021, NON_REVERSE, 0x0000, 0x0000, 0x31C3, ], + [ 'modbus', 'CrcModbus', 0x18005, REVERSE, 0xFFFF, 0x0000, 0x4B37, ], + + # Note definitions of CCITT are disputable. See: + # http://homepages.tesco.net/~rainstorm/crc-catalogue.htm + # http://web.archive.org/web/20071229021252/http://www.joegeluso.com/software/articles/ccitt.htm + [ 'kermit', 'CrcKermit', 0x11021, REVERSE, 0x0000, 0x0000, 0x2189, ], + [ 'crc-ccitt-false', 'CrcCcittFalse', 0x11021, NON_REVERSE, 0xFFFF, 0x0000, 0x29B1, ], + [ 'crc-aug-ccitt', 'CrcAugCcitt', 0x11021, NON_REVERSE, 0x1D0F, 0x0000, 0xE5CC, ], + + [ 'crc-24', 'Crc24', 0x1864CFB, NON_REVERSE, 0xB704CE, 0x000000, 0x21CF02, ], + [ 'crc-24-flexray-a', 'Crc24FlexrayA', 0x15D6DCB, NON_REVERSE, 0xFEDCBA, 0x000000, 0x7979BD, ], + [ 'crc-24-flexray-b', 'Crc24FlexrayB', 0x15D6DCB, NON_REVERSE, 0xABCDEF, 0x000000, 0x1F23B8, ], + + [ 'crc-32', 'Crc32', 0x104C11DB7, REVERSE, 0x00000000, 0xFFFFFFFF, 0xCBF43926, ], + [ 'crc-32-bzip2', 'Crc32Bzip2', 0x104C11DB7, NON_REVERSE, 0x00000000, 0xFFFFFFFF, 0xFC891918, ], + [ 'crc-32c', 'Crc32C', 0x11EDC6F41, REVERSE, 0x00000000, 0xFFFFFFFF, 0xE3069283, ], + [ 'crc-32d', 'Crc32D', 0x1A833982B, REVERSE, 0x00000000, 0xFFFFFFFF, 0x87315576, ], + [ 'crc-32-mpeg', 'Crc32Mpeg', 0x104C11DB7, NON_REVERSE, 0xFFFFFFFF, 0x00000000, 0x0376E6E7, ], + [ 'posix', 'CrcPosix', 0x104C11DB7, NON_REVERSE, 0xFFFFFFFF, 0xFFFFFFFF, 0x765E7680, ], + [ 'crc-32q', 'Crc32Q', 0x1814141AB, NON_REVERSE, 0x00000000, 0x00000000, 0x3010BF7F, ], + [ 'jamcrc', 'CrcJamCrc', 0x104C11DB7, REVERSE, 0xFFFFFFFF, 0x00000000, 0x340BC6D9, ], + [ 'xfer', 'CrcXfer', 0x1000000AF, NON_REVERSE, 0x00000000, 0x00000000, 0xBD0BE338, ], + +# 64-bit +# Name Identifier-name, Poly Reverse Init-value XOR-out Check + [ 'crc-64', 'Crc64', 0x1000000000000001B, REVERSE, 0x0000000000000000, 0x0000000000000000, 0x46A5A9388A5BEFFE, ], # ISO POLY + # See https://lwn.net/Articles/976030/ (MCRC64, used as CRC-64 in the Linux kernel) + [ 'crc-64-2', 'Crc64_2', 0x1000000000000001B, NON_REVERSE, 0x0000000000000000, 0x0000000000000000, 0xE4FFBEA588933790, ], # fag, ISO POLY + [ 'crc-64-go', 'Crc64Go', 0x1000000000000001B, REVERSE, 0x0000000000000000, 0xFFFFFFFFFFFFFFFF, 0xB90956C775A41001, ], # fag, ISO POLY + [ 'crc-64-ecma', 'Crc64Ecma', 0x142F0E1EBA9EA3693, NON_REVERSE, 0x0000000000000000, 0x0000000000000000, 0x6C40DF5F0B497347, ], # fag + [ 'crc-64-we', 'Crc64We', 0x142F0E1EBA9EA3693, NON_REVERSE, 0x0000000000000000, 0xFFFFFFFFFFFFFFFF, 0x62EC59E3F1A4F00A, ], + [ 'crc-64-jones', 'Crc64Jones', 0x1AD93D23594C935A9, REVERSE, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000, 0xCAA717168609F281, ], + [ 'crc-64-xz', 'Crc64Xz', 0x142F0E1EBA9EA3693, REVERSE, 0x0000000000000000, 0xFFFFFFFFFFFFFFFF, 0x995DC9BBDF1939FA, ], # fag, ECMA POLY + [ 'crc-64-redis', 'Crc64Redis', 0x1AD93D23594C935A9, REVERSE, 0x0000000000000000, 0x0000000000000000, 0xE9C6D914C4b8D9CA, ], # fag +] + + +def _simplify_name(name): + """ + Reduce CRC definition name to a simplified form: + * lowercase + * dashes removed + * spaces removed + * any initial "CRC" string removed + """ + name = name.lower() + name = name.replace('-', '') + name = name.replace(' ', '') + if name.startswith('crc'): + name = name[len('crc'):] + return name + + +_crc_definitions_by_name = {} +_crc_definitions_by_identifier = {} +_crc_definitions = [] + +_crc_table_headings = [ 'name', 'identifier', 'poly', 'reverse', 'init', 'xor_out', 'check' ] + +for table_entry in _crc_definitions_table: + crc_definition = dict(zip(_crc_table_headings, table_entry)) + _crc_definitions.append(crc_definition) + name = _simplify_name(table_entry[0]) + if name in _crc_definitions_by_name: + raise Exception("Duplicate entry for '{0}' in CRC table".format(name)) + _crc_definitions_by_name[name] = crc_definition + _crc_definitions_by_identifier[table_entry[1]] = crc_definition + + +def _get_definition_by_name(crc_name): + definition = _crc_definitions_by_name.get(_simplify_name(crc_name), None) + if not definition: + definition = _crc_definitions_by_identifier.get(crc_name, None) + if not definition: + raise KeyError("Unkown CRC name '{0}'".format(crc_name)) + return definition + + +class PredefinedCrc(crcmod.Crc): + def __init__(self, crc_name): + definition = _get_definition_by_name(crc_name) + super().__init__(poly=definition['poly'], initCrc=definition['init'], rev=definition['reverse'], xorOut=definition['xor_out']) + + +# crcmod.predefined.Crc is an alias for crcmod.predefined.PredefinedCrc +Crc = PredefinedCrc + + +def mkPredefinedCrcFun(crc_name): + definition = _get_definition_by_name(crc_name) + return crcmod.mkCrcFun(poly=definition['poly'], initCrc=definition['init'], rev=definition['reverse'], xorOut=definition['xor_out']) + + +# crcmod.predefined.mkCrcFun is an alias for crcmod.predefined.mkPredefinedCrcFun +mkCrcFun = mkPredefinedCrcFun diff -r 4bb2d0975cfe -r a828f320ac58 cutils/crcmod/python3/test.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cutils/crcmod/python3/test.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,541 @@ +#----------------------------------------------------------------------------- +# Copyright (c) 2010 Raymond L. Buvel +# Copyright (c) 2010 Craig McQueen +# Copyright (c) 2025 Franz Glasner +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +#----------------------------------------------------------------------------- +'''Unit tests for crcmod functionality''' + + +import unittest + +from array import array +import binascii + +from .crcmod import mkCrcFun, Crc +from .crcmod import _usingExtension +from .predefined import PredefinedCrc +from .predefined import mkPredefinedCrcFun +from .predefined import _crc_definitions as _predefined_crc_definitions + + +#----------------------------------------------------------------------------- +# This polynomial was chosen because it is the product of two irreducible +# polynomials. +# g8 = (x^7+x+1)*(x+1) +g8 = 0x185 + +#----------------------------------------------------------------------------- +# The following reproduces all of the entries in the Numerical Recipes table. +# This is the standard CCITT polynomial. +g16 = 0x11021 + +#----------------------------------------------------------------------------- +g24 = 0x15D6DCB + +#----------------------------------------------------------------------------- +# This is the standard AUTODIN-II polynomial which appears to be used in a +# wide variety of standards and applications. +g32 = 0x104C11DB7 + + +#----------------------------------------------------------------------------- +# I was able to locate a couple of 64-bit polynomials on the web. To make it +# easier to input the representation, define a function that builds a +# polynomial from a list of the bits that need to be turned on. + +def polyFromBits(bits): + p = 0 + for n in bits: + p = p | (1 << n) + return p + +# The following is from the paper "An Improved 64-bit Cyclic Redundancy Check +# for Protein Sequences" by David T. Jones + +g64a = polyFromBits([64, 63, 61, 59, 58, 56, 55, 52, 49, 48, 47, 46, 44, 41, + 37, 36, 34, 32, 31, 28, 26, 23, 22, 19, 16, 13, 12, 10, 9, 6, 4, + 3, 0]) + +# The following is from Standard ECMA-182 "Data Interchange on 12,7 mm 48-Track +# Magnetic Tape Cartridges -DLT1 Format-", December 1992. + +g64b = polyFromBits([64, 62, 57, 55, 54, 53, 52, 47, 46, 45, 40, 39, 38, 37, + 35, 33, 32, 31, 29, 27, 24, 23, 22, 21, 19, 17, 13, 12, 10, 9, 7, + 4, 1, 0]) + +#----------------------------------------------------------------------------- +# This class is used to check the CRC calculations against a direct +# implementation using polynomial division. + +class poly: + '''Class implementing polynomials over the field of integers mod 2''' + def __init__(self,p): + p = int(p) + if p < 0: raise ValueError('invalid polynomial') + self.p = p + + def __int__(self): + return self.p + + def __eq__(self,other): + return self.p == other.p + + def __ne__(self,other): + return self.p != other.p + + # To allow sorting of polynomials, use their long integer form for + # comparison + def __cmp__(self,other): + return cmp(self.p, other.p) + + def __bool__(self): + return self.p != 0 + + def __neg__(self): + return self # These polynomials are their own inverse under addition + + def __invert__(self): + n = max(self.deg() + 1, 1) + x = (1 << n) - 1 + return poly(self.p ^ x) + + def __add__(self,other): + return poly(self.p ^ other.p) + + def __sub__(self,other): + return poly(self.p ^ other.p) + + def __mul__(self,other): + a = self.p + b = other.p + if a == 0 or b == 0: return poly(0) + x = 0 + while b: + if b&1: + x = x ^ a + a = a<<1 + b = b>>1 + return poly(x) + + def __divmod__(self,other): + u = self.p + m = self.deg() + v = other.p + n = other.deg() + if v == 0: raise ZeroDivisionError('polynomial division by zero') + if n == 0: return (self,poly(0)) + if m < n: return (poly(0),self) + k = m-n + a = 1 << m + v = v << k + q = 0 + while k > 0: + if a & u: + u = u ^ v + q = q | 1 + q = q << 1 + a = a >> 1 + v = v >> 1 + k -= 1 + if a & u: + u = u ^ v + q = q | 1 + return (poly(q),poly(u)) + + def __div__(self,other): + return self.__divmod__(other)[0] + + def __mod__(self,other): + return self.__divmod__(other)[1] + + def __repr__(self): + return 'poly(0x%XL)' % self.p + + def __str__(self): + p = self.p + if p == 0: return '0' + lst = { 0:[], 1:['1'], 2:['x'], 3:['1','x'] }[p&3] + p = p>>2 + n = 2 + while p: + if p&1: lst.append('x^%d' % n) + p = p>>1 + n += 1 + lst.reverse() + return '+'.join(lst) + + def deg(self): + '''return the degree of the polynomial''' + a = self.p + if a == 0: return -1 + n = 0 + while a >= 0x10000: + n += 16 + a = a >> 16 + a = int(a) + while a > 1: + n += 1 + a = a >> 1 + return n + +#----------------------------------------------------------------------------- +# The following functions compute the CRC using direct polynomial division. +# These functions are checked against the result of the table driven +# algorithms. + +g8p = poly(g8) +x8p = poly(1<<8) +def crc8p(d): + p = 0 + for i in d: + p = p*256 + i + p = poly(p) + return int(p*x8p%g8p) + +g16p = poly(g16) +x16p = poly(1<<16) +def crc16p(d): + p = 0 + for i in d: + p = p*256 + i + p = poly(p) + return int(p*x16p%g16p) + +g24p = poly(g24) +x24p = poly(1<<24) +def crc24p(d): + p = 0 + for i in d: + p = p*256 + i + p = poly(p) + return int(p*x24p%g24p) + +g32p = poly(g32) +x32p = poly(1<<32) +def crc32p(d): + p = 0 + for i in d: + p = p*256 + i + p = poly(p) + return int(p*x32p%g32p) + +g64ap = poly(g64a) +x64p = poly(1<<64) +def crc64ap(d): + p = 0 + for i in d: + p = p*256 + i + p = poly(p) + return int(p*x64p%g64ap) + +g64bp = poly(g64b) +def crc64bp(d): + p = 0 + for i in d: + p = p*256 + i + p = poly(p) + return int(p*x64p%g64bp) + + +class KnownAnswerTests(unittest.TestCase): + test_messages = [ + b'T', + b'CatMouse987654321', + ] + + known_answers = [ + [ (g8,0,0), (0xFE, 0x9D) ], + [ (g8,-1,1), (0x4F, 0x9B) ], + [ (g8,0,1), (0xFE, 0x62) ], + [ (g16,0,0), (0x1A71, 0xE556) ], + [ (g16,-1,1), (0x1B26, 0xF56E) ], + [ (g16,0,1), (0x14A1, 0xC28D) ], + [ (g24,0,0), (0xBCC49D, 0xC4B507) ], + [ (g24,-1,1), (0x59BD0E, 0x0AAA37) ], + [ (g24,0,1), (0xD52B0F, 0x1523AB) ], + [ (g32,0,0), (0x6B93DDDB, 0x12DCA0F4) ], + [ (g32,0xFFFFFFFF,1), (0x41FB859F, 0xF7B400A7) ], + [ (g32,0,1), (0x6C0695ED, 0xC1A40EE5) ], + [ (g32,0,1,0xFFFFFFFF), (0xBE047A60, 0x084BFF58) ], + ] + + def test_known_answers(self): + for crcfun_params, v in self.known_answers: + crcfun = mkCrcFun(*crcfun_params) + self.assertEqual(crcfun(b'',0), 0, "Wrong answer for CRC parameters %s, input ''" % (crcfun_params,)) + for i, msg in enumerate(self.test_messages): + self.assertEqual(crcfun(msg), v[i], "Wrong answer for CRC parameters %s, input '%s'" % (crcfun_params,msg)) + self.assertEqual(crcfun(msg[4:], crcfun(msg[:4])), v[i], "Wrong answer for CRC parameters %s, input '%s'" % (crcfun_params,msg)) + self.assertEqual(crcfun(msg[-1:], crcfun(msg[:-1])), v[i], "Wrong answer for CRC parameters %s, input '%s'" % (crcfun_params,msg)) + + +class CompareReferenceCrcTest(unittest.TestCase): + test_messages = [ + b'', + b'T', + b'123456789', + b'CatMouse987654321', + ] + + test_poly_crcs = [ + [ (g8,0,0), crc8p ], + [ (g16,0,0), crc16p ], + [ (g24,0,0), crc24p ], + [ (g32,0,0), crc32p ], + [ (g64a,0,0), crc64ap ], + [ (g64b,0,0), crc64bp ], + ] + + @staticmethod + def reference_crc32(d, crc=0): + """This function modifies the return value of binascii.crc32 + to be an unsigned 32-bit value. I.e. in the range 0 to 2**32-1.""" + # Work around the future warning on constants. + if crc > 0x7FFFFFFF: + x = int(crc & 0x7FFFFFFF) + crc = x | -2147483648 + x = binascii.crc32(d,crc) + return int(x) & 0xFFFFFFFF + + def test_compare_crc32(self): + """The binascii module has a 32-bit CRC function that is used in a wide range + of applications including the checksum used in the ZIP file format. + This test compares the CRC-32 implementation of this crcmod module to + that of binascii.crc32.""" + # The following function should produce the same result as + # self.reference_crc32 which is derived from binascii.crc32. + crc32 = mkCrcFun(g32,0,1,0xFFFFFFFF) + + for msg in self.test_messages: + self.assertEqual(crc32(msg), self.reference_crc32(msg)) + + def test_compare_poly(self): + """Compare various CRCs of this crcmod module to a pure + polynomial-based implementation.""" + for crcfun_params, crc_poly_fun in self.test_poly_crcs: + # The following function should produce the same result as + # the associated polynomial CRC function. + crcfun = mkCrcFun(*crcfun_params) + + for msg in self.test_messages: + self.assertEqual(crcfun(msg), crc_poly_fun(msg)) + + +class CrcClassTest(unittest.TestCase): + """Verify the Crc class""" + + msg = b'CatMouse987654321' + + def test_simple_crc32_class(self): + """Verify the CRC class when not using xorOut""" + crc = Crc(g32) + + str_rep = \ +'''poly = 0x104C11DB7 +reverse = True +initCrc = 0xFFFFFFFF +xorOut = 0x00000000 +crcValue = 0xFFFFFFFF''' + self.assertEqual(str(crc), str_rep) + self.assertEqual(crc.digest(), b'\xff\xff\xff\xff') + self.assertEqual(crc.hexdigest(), 'FFFFFFFF') + + crc.update(self.msg) + self.assertEqual(crc.crcValue, 0xF7B400A7) + self.assertEqual(crc.digest(), b'\xf7\xb4\x00\xa7') + self.assertEqual(crc.hexdigest(), 'F7B400A7') + + # Verify the .copy() method + x = crc.copy() + self.assertTrue(x is not crc) + str_rep = \ +'''poly = 0x104C11DB7 +reverse = True +initCrc = 0xFFFFFFFF +xorOut = 0x00000000 +crcValue = 0xF7B400A7''' + self.assertEqual(str(crc), str_rep) + self.assertEqual(str(x), str_rep) + + def test_full_crc32_class(self): + """Verify the CRC class when using xorOut""" + + crc = Crc(g32, initCrc=0, xorOut= ~0) + + str_rep = \ +'''poly = 0x104C11DB7 +reverse = True +initCrc = 0x00000000 +xorOut = 0xFFFFFFFF +crcValue = 0x00000000''' + self.assertEqual(str(crc), str_rep) + self.assertEqual(crc.digest(), b'\x00\x00\x00\x00') + self.assertEqual(crc.hexdigest(), '00000000') + + crc.update(self.msg) + self.assertEqual(crc.crcValue, 0x84BFF58) + self.assertEqual(crc.digest(), b'\x08\x4b\xff\x58') + self.assertEqual(crc.hexdigest(), '084BFF58') + + # Verify the .copy() method + x = crc.copy() + self.assertTrue(x is not crc) + str_rep = \ +'''poly = 0x104C11DB7 +reverse = True +initCrc = 0x00000000 +xorOut = 0xFFFFFFFF +crcValue = 0x084BFF58''' + self.assertEqual(str(crc), str_rep) + self.assertEqual(str(x), str_rep) + + # Verify the .new() method + y = crc.new() + self.assertTrue(y is not crc) + self.assertTrue(y is not x) + str_rep = \ +'''poly = 0x104C11DB7 +reverse = True +initCrc = 0x00000000 +xorOut = 0xFFFFFFFF +crcValue = 0x00000000''' + self.assertEqual(str(y), str_rep) + + +class PredefinedCrcTest(unittest.TestCase): + """Verify the predefined CRCs""" + + test_messages_for_known_answers = [ + b'', # Test cases below depend on this first entry being the empty string. + b'T', + b'CatMouse987654321', + ] + + known_answers = [ + [ 'crc-aug-ccitt', (0x1D0F, 0xD6ED, 0x5637) ], + [ 'x-25', (0x0000, 0xE4D9, 0x0A91) ], + [ 'crc-32', (0x00000000, 0xBE047A60, 0x084BFF58) ], + ] + + def test_known_answers(self): + for crcfun_name, v in self.known_answers: + crcfun = mkPredefinedCrcFun(crcfun_name) + self.assertEqual(crcfun(b'',0), 0, "Wrong answer for CRC '%s', input ''" % crcfun_name) + for i, msg in enumerate(self.test_messages_for_known_answers): + self.assertEqual(crcfun(msg), v[i], "Wrong answer for CRC %s, input '%s'" % (crcfun_name,msg)) + self.assertEqual(crcfun(msg[4:], crcfun(msg[:4])), v[i], "Wrong answer for CRC %s, input '%s'" % (crcfun_name,msg)) + self.assertEqual(crcfun(msg[-1:], crcfun(msg[:-1])), v[i], "Wrong answer for CRC %s, input '%s'" % (crcfun_name,msg)) + + def test_class_with_known_answers(self): + for crcfun_name, v in self.known_answers: + for i, msg in enumerate(self.test_messages_for_known_answers): + crc1 = PredefinedCrc(crcfun_name) + crc1.update(msg) + self.assertEqual(crc1.crcValue, v[i], "Wrong answer for crc1 %s, input '%s'" % (crcfun_name,msg)) + + crc2 = crc1.new() + # Check that crc1 maintains its same value, after .new() call. + self.assertEqual(crc1.crcValue, v[i], "Wrong state for crc1 %s, input '%s'" % (crcfun_name,msg)) + # Check that the new class instance created by .new() contains the initialisation value. + # This depends on the first string in self.test_messages_for_known_answers being + # the empty string. + self.assertEqual(crc2.crcValue, v[0], "Wrong state for crc2 %s, input '%s'" % (crcfun_name,msg)) + + crc2.update(msg) + # Check that crc1 maintains its same value, after crc2 has called .update() + self.assertEqual(crc1.crcValue, v[i], "Wrong state for crc1 %s, input '%s'" % (crcfun_name,msg)) + # Check that crc2 contains the right value after calling .update() + self.assertEqual(crc2.crcValue, v[i], "Wrong state for crc2 %s, input '%s'" % (crcfun_name,msg)) + + def test_function_predefined_table(self): + for table_entry in _predefined_crc_definitions: + # Check predefined function + crc_func = mkPredefinedCrcFun(table_entry['name']) + calc_value = crc_func(b"123456789") + self.assertEqual(calc_value, table_entry['check'], "Wrong answer for CRC '%s'" % table_entry['name']) + + def test_class_predefined_table(self): + for table_entry in _predefined_crc_definitions: + # Check predefined class + crc1 = PredefinedCrc(table_entry['name']) + crc1.update(b"123456789") + self.assertEqual(crc1.crcValue, table_entry['check'], "Wrong answer for CRC '%s'" % table_entry['name']) + + +class InputTypesTest(unittest.TestCase): + """Check the various input types that CRC functions can accept.""" + + msg = b'CatMouse987654321' + + check_crc_names = [ + 'crc-aug-ccitt', + 'x-25', + 'crc-32', + ] + + array_check_types = [ + 'B', + 'H', + 'I', + 'L', + ] + + def test_bytearray_input(self): + """Test that bytearray inputs are accepted, as an example + of a type that implements the buffer protocol.""" + for crc_name in self.check_crc_names: + crcfun = mkPredefinedCrcFun(crc_name) + for i in range(len(self.msg) + 1): + test_msg = self.msg[:i] + bytes_answer = crcfun(test_msg) + bytearray_answer = crcfun(bytearray(test_msg)) + self.assertEqual(bytes_answer, bytearray_answer) + + def test_array_input(self): + """Test that array inputs are accepted, as an example + of a type that implements the buffer protocol.""" + for crc_name in self.check_crc_names: + crcfun = mkPredefinedCrcFun(crc_name) + for i in range(len(self.msg) + 1): + test_msg = self.msg[:i] + bytes_answer = crcfun(test_msg) + for array_type in self.array_check_types: + if i % array(array_type).itemsize == 0: + test_array = array(array_type, test_msg) + array_answer = crcfun(test_array) + self.assertEqual(bytes_answer, array_answer) + + def test_unicode_input(self): + """Test that Unicode input raises TypeError""" + for crc_name in self.check_crc_names: + crcfun = mkPredefinedCrcFun(crc_name) + with self.assertRaises(TypeError): + crcfun("123456789") + + +def runtests(): + print("Using extension:", _usingExtension) + print() + unittest.main() + + +if __name__ == '__main__': + runtests() diff -r 4bb2d0975cfe -r a828f320ac58 cutils/crcmod/test.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cutils/crcmod/test.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,40 @@ +# -*- coding: utf-8; -*- +#----------------------------------------------------------------------------- +# Copyright (c) 2010 Raymond L. Buvel +# Copyright (c) 2010 Craig McQueen +# Copyright (c) 2025 Franz Glasner +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +#----------------------------------------------------------------------------- + +from __future__ import absolute_import + +import sys + +if sys.version_info[0] < 3: + from .python2.test import * +else: + from .python3.test import * + + +del sys + + +if __name__ == '__main__': + runtests() diff -r 4bb2d0975cfe -r a828f320ac58 cutils/dos2unix.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cutils/dos2unix.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# :- +# :Copyright: (c) 2020-2025 Franz Glasner +# :License: BSD-3-Clause +# :- +r"""Pure Python implementation of `dos2unix`. + +""" + +from __future__ import print_function, absolute_import + +from . import (__version__, __revision__) + + +__all__ = [] + + +import argparse +import io +import sys + + +def main(argv=None): + aparser = argparse.ArgumentParser( + description="Python implementation of dos2unix", + fromfile_prefix_chars='@') + aparser.add_argument( + "--version", "-V", action="version", + version="%s (rv:%s)" % (__version__, __revision__)) + aparser.add_argument( + "--keepdate", "-k", action="store_true", + help="Keep the date stamp of output file same as input file.") + aparser.add_argument( + "--oldfile", "-o", action="store_false", dest="newfile", default=False, + help="Old file mode. Convert the file and write output to it." + " The program defaults to run in this mode." + " Wildcard names may be used. ") + aparser.add_argument( + "--newfile", "-n", action="store_true", dest="newfile", default=False, + help="New file mode. Convert the infile and write output to outfile." + " File names must be given in pairs and wildcard names should" + " NOT be used or you WILL lose your files.") + aparser.add_argument( + "--quiet", "-q", action="store_true", + help="Quiet mode. Suppress all warning and messages.") + + aparser.add_argument( + "files", nargs="+", metavar="FILE") + + opts = aparser.parse_args(args=argv) + + if opts.keepdate: + raise NotImplementedError("--keepdate, -k") + + return dos2unix(opts) + + +def gen_opts(files=[], newfile=False, keepdate=False, quiet=True): + if keepdate: + raise NotImplementedError("--keepdate, -k") + + if newfile and (len(files) % 2): + raise ValueError("need pairs of files") + + opts = argparse.Namespace(files=files, + newfile=newfile, + keepdate=keepdate, + quiet=quiet) + return opts + + +def dos2unix(opts): + if opts.newfile: + return _convert_copy(opts) + else: + return _convert_inplace(opts) + + +def _convert_inplace(opts): + lines = [] + for filename in opts.files: + with io.open(filename, "rt", encoding="iso-8859-1") as source: + for line in source: + lines.append(line.encode("iso-8859-1")) + with open(filename, "wb") as dest: + for line in lines: + dest.write(line) + + +def _convert_copy(opts): + if len(opts.files) % 2: + print("ERROR: need pairs of files", file=sys.stderr) + return 64 # :manpage:`sysexits(3)` EX_USAGE + idx = 0 + while idx < len(opts.files): + with io.open(opts.files[idx], "rt", encoding="iso-8859-1") as source: + with open(opts.files[idx+1], "wb") as dest: + for line in source: + dest.write(line.encode("iso-8859-1")) + idx += 2 + + +if __name__ == "__main__": + sys.exit(main()) diff -r 4bb2d0975cfe -r a828f320ac58 cutils/shasum.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cutils/shasum.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,456 @@ +# -*- coding: utf-8 -*- +# :- +# :Copyright: (c) 2020-2025 Franz Glasner +# :License: BSD-3-Clause +# :- +r"""Pure Python implementation of `shasum`. + +""" + +from __future__ import print_function, absolute_import + + +__all__ = [] + + +import argparse +import base64 +import binascii +import errno +import io +import os +import re +import sys + +from . import (__version__, __revision__) +from . import util +from .util import digest as digestmod + + +def main(argv=None): + aparser = argparse.ArgumentParser( + description="Python implementation of shasum", + fromfile_prefix_chars='@') + aparser.add_argument( + "--algorithm", "-a", action="store", type=util.argv2algo, + help="""1 (default, aka sha1), 224, 256, 384, 512, +3 (alias for sha3-512), 3-224, 3-256, 3-384, 3-512, +blake2b, blake2b-256, blake2s, blake2 (alias for blake2b), +blake2-256 (alias for blake2b-256), md5""") + aparser.add_argument( + "--base64", action="store_true", + help="Output checksums in base64 notation, not hexadecimal (OpenBSD).") + aparser.add_argument( + "--binary", "-b", action="store_false", dest="text_mode", + default=False, + help="Read in binary mode (default)") + aparser.add_argument( + "--bsd", "-B", action="store_true", dest="bsd", default=False, + help="""Write BSD style output. This is also the default output format +of :command:`openssl dgst`.""") + aparser.add_argument( + "--check", "-c", action="store_true", + help="""Read digests from FILEs and check them. +If this option is specified, the FILE options become checklists. Each +checklist should contain hash results in a supported format, which will +be verified against the specified paths. Output consists of the digest +used, the file name, and an OK, FAILED, or MISSING for the result of +the comparison. This will validate any of the supported checksums. +If no file is given, stdin is used.""") + aparser.add_argument( + "--checklist", "-C", metavar="CHECKLIST", + help="""Compare the checksum of each FILE against the checksums in +the CHECKLIST. Any specified FILE that is not listed in the CHECKLIST will +generate an error.""") + aparser.add_argument( + "--checklist-allow-distinfo", action="store_true", + dest="allow_distinfo", + help='''Allow FreeBSD "distinfo" formatted checklists: +ignore SIZE and TIMESTAMP lines.''') + + aparser.add_argument( + "--follow-symlinks", action="store_true", dest="follow_symlinks", + help="""Also follow symlinks that resolve to directories. +Only effective if `--recurse` is activated.""") + + aparser.add_argument( + "--mmap", action="store_true", dest="mmap", default=None, + help="""Use mmap if available. Default is to determine automatically + from the filesize.""") + aparser.add_argument( + "--no-mmap", action="store_false", dest="mmap", default=None, + help="Dont use mmap.") + + aparser.add_argument( + "--recurse", action="store_true", + help="""Recurse into sub-directories while interpreting every +FILE as a directory.""") + + aparser.add_argument( + "--reverse", "-r", action="store_false", dest="bsd", default=False, + help="""Explicitely select normal coreutils style output +(to be option compatible with BSD style commands and +:command:`openssl dgst -r`)""") + aparser.add_argument( + "--tag", action="store_true", dest="bsd", default=False, + help="""Alias for the `--bsd' option (to be compatible with +:command:`b2sum`)""") + aparser.add_argument( + "--text", "-t", action="store_true", dest="text_mode", default=False, + help="Read in text mode (not supported)") + aparser.add_argument( + "--version", "-v", action="version", + version="%s (rv:%s)" % (__version__, __revision__)) + aparser.add_argument( + "files", nargs="*", metavar="FILE") + + opts = aparser.parse_args(args=argv) + + if opts.text_mode: + print("ERROR: text mode not supported", file=sys.stderr) + sys.exit(78) # :manpage:`sysexits(3)` EX_CONFIG + + if opts.check and opts.checklist: + print("ERROR: only one of --check or --checklist allowed", + file=sys.stderr) + sys.exit(64) # :manpage:`sysexits(3)` EX_USAGE + + if not opts.algorithm: + opts.algorithm = util.argv2algo("1") + + opts.dest = None + + return shasum(opts) + + +def gen_opts(files=[], algorithm="SHA1", bsd=False, text_mode=False, + checklist=False, check=False, dest=None, base64=False, + allow_distinfo=False, mmap=None, recurse=False, + follow_symlinks=False): + if text_mode: + raise ValueError("text mode not supported") + if checklist and check: + raise ValueError("only one of `checklist' or `check' is allowed") + opts = argparse.Namespace(files=files, + algorithm=(util.algotag2algotype(algorithm), + algorithm), + bsd=bsd, + checklist=checklist, + check=check, + text_mode=False, + dest=dest, + base64=base64, + allow_distinfo=allow_distinfo, + mmap=mmap, + recurse=recurse, + follow_symlinks=follow_symlinks) + return opts + + +def shasum(opts): + if opts.check: + return verify_digests_from_files(opts) + elif opts.checklist: + return verify_digests_with_checklist(opts) + else: + return generate_digests(opts) + + +def generate_digests(opts): + if opts.bsd: + out = out_bsd + else: + out = out_std + if opts.recurse: + if not opts.files: + opts.files.append(".") + for dn in opts.files: + if not os.path.isdir(dn): + if os.path.exists(dn): + raise OSError(errno.ENOTDIR, "not a directory", dn) + else: + raise OSError(errno.ENOENT, "directory does not exist", dn) + for dirpath, dirnames, dirfiles in os.walk( + dn, followlinks=opts.follow_symlinks): + dirnames.sort() + dirfiles.sort() + for fn in dirfiles: + path = os.path.join(dirpath, fn) + out(opts.dest or sys.stdout, + digestmod.compute_digest_file( + opts.algorithm[0], path, use_mmap=opts.mmap), + path, + opts.algorithm[1], + True, + opts.base64) + else: + if not opts.files or (len(opts.files) == 1 and opts.files[0] == '-'): + if util.PY2: + if sys.platform == "win32": + import msvcrt # noqa: E401 + msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) + source = sys.stdin + else: + source = sys.stdin.buffer + out(sys.stdout, + digestmod.compute_digest_stream(opts.algorithm[0], source), + None, + opts.algorithm[1], + True, + opts.base64) + else: + for fn in opts.files: + out(opts.dest or sys.stdout, + digestmod.compute_digest_file( + opts.algorithm[0], fn, use_mmap=opts.mmap), + fn, + opts.algorithm[1], + True, + opts.base64) + return 0 + + +def compare_digests_equal(given_digest, expected_digest, algo): + """Compare a newly computed binary digest `given_digest` with a digest + string (hex or base64) in `expected_digest`. + + :param bytes given_digest: + :param expected_digest: digest (as bytes) or hexlified or base64 encoded + digest (as str) + :type expected_digest: str or bytes or bytearray + :param algo: The algorithm (factory) + :return: `True` if the digests are equal, `False` if not + :rtype: bool + + """ + if isinstance(expected_digest, (bytes, bytearray)) \ + and len(expected_digest) == algo().digest_size: + exd = expected_digest + else: + if len(expected_digest) == algo().digest_size * 2: + # hex + if re.search(r"\A[a-fA-F0-9]+\Z", expected_digest): + try: + exd = binascii.unhexlify(expected_digest) + except TypeError: + return False + else: + return False + else: + # base64 + if re.search( + r"\A(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?\Z", # noqa: E501 line too long + expected_digest): + try: + exd = base64.b64decode(expected_digest) + except TypeError: + return False + else: + return False + return given_digest == exd + + +def verify_digests_with_checklist(opts): + dest = opts.dest or sys.stdout + exit_code = 0 + if not opts.files or (len(opts.files) == 1 and opts.files[0] == '-'): + if util.PY2: + if sys.platform == "win32": + import os, msvcrt # noqa: E401 + msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) + source = sys.stdin + else: + source = sys.stdin.buffer + pl = get_parsed_digest_line_from_checklist(opts.checklist, opts, None) + if pl is None: + exit_code = 1 + print("-: MISSING", file=dest) + else: + tag, algo, cl_filename, cl_digest = pl + computed_digest = digestmod.compute_digest_stream(algo, source) + if compare_digests_equal(computed_digest, cl_digest, algo): + res = "OK" + else: + res = "FAILED" + exit_code = 1 + print("{}: {}: {}".format(tag, "-", res), file=dest) + else: + for fn in opts.files: + pl = get_parsed_digest_line_from_checklist( + opts.checklist, opts, fn) + if pl is None: + print("{}: MISSING".format(fn), file=dest) + exit_code = 1 + else: + tag, algo, cl_filename, cl_digest = pl + computed_digest = digestmod.compute_digest_file( + algo, fn, use_mmap=opts.mmap) + if compare_digests_equal(computed_digest, cl_digest, algo): + res = "OK" + else: + exit_code = 1 + res = "FAILED" + print("{}: {}: {}".format(tag, fn, res), file=dest) + return exit_code + + +def verify_digests_from_files(opts): + dest = opts.dest or sys.stdout + exit_code = 0 + if not opts.files or (len(opts.files) == 1 and opts.files[0] == '-'): + for checkline in sys.stdin: + if not checkline: + continue + r, fn, tag = handle_checkline(opts, checkline) + if tag in ("SIZE", "TIMESTAMP"): + assert opts.allow_distinfo + continue + print("{}: {}: {}".format(tag, fn, r.upper()), file=dest) + if r != "ok" and exit_code == 0: + exit_code = 1 + else: + for fn in opts.files: + with io.open(fn, "rt", encoding="utf-8") as checkfile: + for checkline in checkfile: + if not checkline: + continue + r, fn, tag = handle_checkline(opts, checkline) + if tag in ("SIZE", "TIMESTAMP"): + assert opts.allow_distinfo + continue + print("{}: {}: {}".format(tag, fn, r.upper()), file=dest) + if r != "ok" and exit_code == 0: + exit_code = 1 + return exit_code + + +def handle_checkline(opts, line): + """ + :return: a tuple with static "ok", "missing", or "failed", the filename and + the digest used + :rtype: tuple(str, str, str) + + """ + parts = parse_digest_line(opts, line) + if not parts: + raise ValueError( + "improperly formatted digest line: {}".format(line)) + tag, algo, fn, digest = parts + if tag in ("SIZE", "TIMESTAMP"): + assert opts.allow_distinfo + return (None, None, tag) + try: + d = digestmod.compute_digest_file(algo, fn, use_mmap=opts.mmap) + if compare_digests_equal(d, digest, algo): + return ("ok", fn, tag) + else: + return ("failed", fn, tag) + except EnvironmentError: + return ("missing", fn, tag) + + +def get_parsed_digest_line_from_checklist(checklist, opts, filename): + if filename is None: + filenames = ("-", "stdin", "", ) + else: + filenames = ( + util.normalize_filename(filename, strip_leading_dot_slash=True),) + with io.open(checklist, "rt", encoding="utf-8") as clf: + for checkline in clf: + if not checkline: + continue + parts = parse_digest_line(opts, checkline) + if not parts: + raise ValueError( + "improperly formatted digest line: {}".format(checkline)) + if parts[0] in ("SIZE", "TIMESTAMP"): + assert opts.allow_distinfo + continue + fn = util.normalize_filename( + parts[2], strip_leading_dot_slash=True) + if fn in filenames: + return parts + else: + return None + + +def parse_digest_line(opts, line): + """Parse a `line` of a digest file and return its parts. + + This is rather strict. But if `opts.allow_distinfo` is `True` then + some additional keywords ``SIZE`` and ``TIMESTAMP``are recignized + and returned. The caller is responsible to handle them. + + :return: a tuple of the normalized algorithm tag, the algorithm + constructor, the filename and the hex digest; + if `line` cannot be parsed successfully `None` is returned + :rtype: tuple(str, obj, str, str) or None + + Handles coreutils and BSD-style file formats. + + """ + # determine checkfile format (BSD or coreutils) + # BSD? + mo = re.search(r"\A(\S+)\s*\((.*)\)\s*=\s*(.+)\n?\Z", line) + if mo: + # (tag, algorithm, filename, digest) + if opts.allow_distinfo: + if mo.group(1) == "SIZE": + return ("SIZE", None, None, mo.group(3)) + return (mo.group(1), + util.algotag2algotype(mo.group(1)), + mo.group(2), + mo.group(3)) + else: + if opts.allow_distinfo: + mo = re.search(r"\ATIMESTAMP\s*=\s*([0-9]+)\s*\n\Z", line) + if mo: + return ("TIMESTAMP", None, None, mo.group(1)) + + # coreutils? + mo = re.search(r"([^\ ]+) [\*\ ]?(.+)\n?\Z", line) + if mo: + # (tag, algorithm, filename, digest) + return (opts.algorithm[1], + opts.algorithm[0], + mo.group(2), + mo.group(1)) + else: + return None + + +def out_bsd(dest, digest, filename, digestname, binary, use_base64): + """BSD format output, also :command:`openssl dgst` and + :command:`b2sum --tag" format output + + """ + if use_base64: + digest = base64.b64encode(digest).decode("ascii") + else: + digest = binascii.hexlify(digest).decode("ascii") + if filename is None: + print(digest, file=dest) + else: + print("{} ({}) = {}".format(digestname, + util.normalize_filename(filename), + digest), + file=dest) + + +def out_std(dest, digest, filename, digestname, binary, use_base64): + """Coreutils format (:command:`shasum` et al.) + + """ + if use_base64: + digest = base64.b64encode(digest).decode("ascii") + else: + digest = binascii.hexlify(digest).decode("ascii") + print("{} {}{}".format( + digest, + '*' if binary else ' ', + '-' if filename is None else util.normalize_filename(filename)), + file=dest) + + +if __name__ == "__main__": + sys.exit(main()) diff -r 4bb2d0975cfe -r a828f320ac58 cutils/treesum.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cutils/treesum.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,1334 @@ +# -*- coding: utf-8 -*- +# :- +# :Copyright: (c) 2020-2025 Franz Glasner +# :License: BSD-3-Clause +# :- +r"""Generate and verify checksums for directory trees. + +""" + +from __future__ import print_function, absolute_import + + +__all__ = [] + + +import argparse +import base64 +import binascii +import collections +import datetime +import logging +import os +import re +import stat +import sys +import time +import zlib + +from . import (__version__, __revision__) +from . import util +from .util import cm +from .util import digest +from .util import walk + + +def main(argv=None): + + def _populate_generate_arguments(gp): + """Use to populate command aliases. + + This is because :class:`argparse.ArgumentParser` does not + support them for all supported Python versions. + + """ + gp.add_argument( + "--algorithm", "-a", action="store", type=util.argv2algo, + help="1 (aka sha1), 224, 256 (aka sha256), 384, 512 (aka sha512), " + "3 (alias for sha3-512), 3-224, 3-256, 3-384, 3-512, " + "blake2b, blake2b-256, blake2s, " + "blake2 (alias for blake2b), " + "blake2-256 (alias for blake2b-256), " + "md5. " + "The default depends on availability in hashlib: " + "blake2b-256, sha256 or sha1.") + gp.add_argument( + "--append-output", action="store_true", dest="append_output", + help="Append to the output file instead of overwriting it.") + gp.add_argument( + "--base64", action="store_true", + help="Output checksums in base64 notation, not hexadecimal " + "(OpenBSD).") + gp.add_argument( + "--comment", action="append", default=[], + help="Put given comment COMMENT into the output as \"COMMENT\". " + "Can be given more than once.") + gp.add_argument( + "--follow-directory-symlinks", "-l", action=SymlinkAction, + const="follow-directory-symlinks", + default=FollowSymlinkConfig(False, False, True), + dest="follow_symlinks", + help="""Follow symbolic links to directories when walking a +directory tree. Augments --physical and -p.""") + gp.add_argument( + "--follow-file-symlinks", action=SymlinkAction, + const="follow-file-symlinks", + default=FollowSymlinkConfig(False, False, True), + dest="follow_symlinks", + help="""Follow symbolic links to files when walking a +directory tree. Augments --physical.""") + gp.add_argument( + "--full-mode", action="store_true", dest="metadata_full_mode", + help="Consider all mode bits as returned from stat(2) when " + "computing directory digests. " + "Note that mode bits on symbolic links itself are not " + "considered.") + gp.add_argument( + "--generator", choices=("normal", "full", "none"), + default="normal", + help="""Put a `GENERATOR' line into the output. +`full' prints full Python and OS/platform version information, +`normal' prints just whether Python 2 or Python 3 is used, and `none' +suppresses the output completely. The default is `normal'.""") + gp.add_argument( + "--logical", "-L", action=SymlinkAction, dest="follow_symlinks", + const=FollowSymlinkConfig(True, True, True), + help="""Follow symbolic links everywhere: on command line +arguments and -- while walking -- directory and file symbolic links. +Overwrites any other symlink related options +(--physical,-p, no-follow-directory-symlinks, no-follow-file-symlinks, +et al.). +""") + gp.add_argument( + "--minimal", nargs="?", const="", default=None, metavar="TAG", + help="Produce minimal output only. If a TAG is given and not " + "empty use it as the leading \"ROOT ()\" output.") + gp.add_argument( + "--mmap", action="store_true", dest="mmap", default=None, + help="Use mmap if available. Default is to determine " + "automatically from the filesize.") + gp.add_argument( + "--mode", action="store_true", dest="metadata_mode", + help="Consider the permission bits of stat(2) using S_IMODE (i.e. " + "all bits without the filetype bits) when " + "computing directory digests. Note that mode bits on " + "symbolic links itself are not considered.") + gp.add_argument( + "--mtime", action="store_true", dest="metadata_mtime", + help="Consider the mtime of files (non-directories) when " + "generating digests for directories. Digests for files are " + "not affected.") + gp.add_argument( + "--no-follow-directory-symlinks", action=SymlinkAction, + const="no-follow-directory-symlinks", + dest="follow_symlinks", + help="""Do not follow symbolic links to directories when walking a +directory tree. Augments --logical.""") + gp.add_argument( + "--no-follow-file-symlinks", action=SymlinkAction, + const="no-follow-file-symlinks", + dest="follow_symlinks", + help="""Dont follow symbolic links to files when walking a +directory tree. Augments --logical and -p.""") + gp.add_argument( + "--no-mmap", action="store_false", dest="mmap", default=None, + help="Dont use mmap.") + gp.add_argument( + "--output", "-o", action="store", metavar="OUTPUT", + help="Put the checksum into given file. " + "If not given or if it is given as `-' then stdout is used.") + gp.add_argument( + "--physical", "-P", action=SymlinkAction, dest="follow_symlinks", + const=FollowSymlinkConfig(False, False, False), + help="""Do not follow any symbolic links whether they are given +on the command line or when walking the directory tree. +Overwrites any other symlink related options +(--logical, -p, follow-directory-symlinks, follow-file-symlinks, et al.). +This is the default.""") + gp.add_argument( + "-p", action=SymlinkAction, dest="follow_symlinks", + const=FollowSymlinkConfig(False, False, True), + help="""Do not follow any symbolic links to directories, +whether they are given on the command line or when walking the directory tree, +but follow symbolic links to files. +Overwrites any other symlink related options +(--logical, --physical, follow-directory-symlinks, no-follow-file-symlinks, +et al.). +This is the default.""") + gp.add_argument( + "--print-size", action="store_true", + help="""Print the size of a file or the accumulated sizes of +directory content into the output also. +The size is not considered when computing digests. For symbolic links +the size is not printed also.""") + gp.add_argument( + "--size-only", action="store_true", + help="""Print only the size of files and for each directory its +accumulated directory size. Digests are not computed.""") + gp.add_argument( + "--utf8", "--utf-8", action="store_true", + help="""Encode all file paths using UTF-8 instead of +the filesystem encoding. Add some error tag into the path if it cannot +representated in Unicode cleanly.""") + gp.add_argument( + "directories", nargs="*", metavar="DIRECTORY") + + def _populate_info_arguments(ip): + ip.add_argument( + "--last", action="store_true", dest="print_only_last_block", + help="Print only the last block of every given input file") + ip.add_argument( + "digest_files", nargs="+", metavar="TREESUM-DIGEST-FILE") + + parser = argparse.ArgumentParser( + description="Generate and verify checksums for directory trees.", + fromfile_prefix_chars='@', + add_help=False) + + # + # Global options for all sub-commands. + # In a group because this allows a customized title. + # + gparser = parser.add_argument_group(title="Global Options") + gparser.add_argument( + "--debug", action="store_true", + help="Activate debug logging to stderr") + gparser.add_argument( + "-v", "--version", action="version", + version="%s (rv:%s)" % (__version__, __revision__), + help="Show program's version number and exit") + gparser.add_argument( + "-h", "--help", action="help", + help="Show this help message and exit") + + # + # Subcommands + # + subparsers = parser.add_subparsers( + dest="subcommand", + title="Commands", + description="This tool uses subcommands. " + "To see detailed help for a specific subcommand use " + "the -h/--help option after the subcommand name. " + "A list of valid commands and their short descriptions " + "is listed below:", + metavar="COMMAND") + + genparser = subparsers.add_parser( + "generate", + help="Generate checksums for directory trees", + description="Generate checksums for directory trees.") + _populate_generate_arguments(genparser) + # And an alias for "generate" + genparser2 = subparsers.add_parser( + "gen", + help="Alias for \"generate\"", + description="Generate checksums for directory trees. " + "This is an alias to \"generate\".") + _populate_generate_arguments(genparser2) + + infoparser = subparsers.add_parser( + "info", + help="Print some information from given treesum digest file", + description="""Print some informations from given treesum digest files +to stdout.""" + ) + _populate_info_arguments(infoparser) + + hparser = subparsers.add_parser( + "help", + help="Show this help message or a subcommand's help and exit", + description="Show this help message or a subcommand's help and exit.") + hparser.add_argument("help_command", nargs='?', metavar="COMMAND") + + vparser = subparsers.add_parser( + "version", + help="Show the program's version number and exit", + description="Show the program's version number and exit.") + + # Parse leniently to just check for "version" and/or help + opts, _dummy = parser.parse_known_args(args=argv) + + if opts.subcommand == "version": + print("%s (rv:%s)" % (__version__, __revision__), + file=sys.stdout) + return 0 + if opts.subcommand == "help": + if not opts.help_command: + parser.print_help() + else: + if opts.help_command == "generate": + genparser.print_help() + elif opts.help_command == "gen": + genparser2.print_help() + elif opts.help_command == "info": + infoparser.print_help() + elif opts.help_command == "version": + vparser.print_help() + elif opts.help_command == "help": + hparser.print_help() + else: + parser.print_help() + return 0 + + # Reparse strictly + opts = parser.parse_args(args=argv) + + # Minimal logging -- just for debugging - not for more "normal" use + logging.basicConfig( + level=logging.DEBUG if opts.debug else logging.WARNING, + stream=sys.stderr, + format="[%(asctime)s][%(levelname)s][%(process)d:%(name)s] %(message)s" + ) + logging.captureWarnings(True) + + return treesum(opts) + + +FollowSymlinkConfig = collections.namedtuple("FollowSymlinkConfig", + ["command_line", + "directory", + "file"]) + + +class SymlinkAction(argparse.Action): + + """`type' is fixed here. + `dest' is a tuple with three items: + + 1. follow symlinks on the command line + 2. follow directory symlinks while walking + 3. follow file symlinks while walking (not yet implemented) + + """ + + def __init__(self, *args, **kwargs): + if "nargs" in kwargs: + raise ValueError("`nargs' not allowed") + if "type" in kwargs: + raise ValueError("`type' not allowed") + c = kwargs.get("const", None) + if c is None: + raise ValueError("a const value is needed") + if (not isinstance(c, FollowSymlinkConfig) + and c not in ("follow-directory-symlinks", + "no-follow-directory-symlinks", + "follow-file-symlinks", + "no-follow-file-symlinks")): + raise ValueError( + "invalid value for the `const' configuration value") + default = kwargs.get("default", None) + if (default is not None + and not isinstance(default, FollowSymlinkConfig)): + raise TypeError("invalid type for `default'") + kwargs["nargs"] = 0 + super(SymlinkAction, self).__init__(*args, **kwargs) + + def __call__(self, parser, namespace, values, option_string=None): + curval = getattr(namespace, self.dest, None) + if curval is None: + curval = FollowSymlinkConfig(False, False, True) + if isinstance(self.const, FollowSymlinkConfig): + curval = self.const + else: + if self.const == "follow-directory-symlinks": + curval = FollowSymlinkConfig( + curval.command_line, True, curval.file) + elif self.const == "no-follow-directory-symlinks": + curval = FollowSymlinkConfig( + curval.command_line, False, curval.file) + elif self.const == "follow-file-symlinks": + curval = FollowSymlinkConfig( + curval.command_line, curval.directory, True) + elif self.const == "no-follow-file-symlinks": + curval = FollowSymlinkConfig( + curval.command_line, curval.directory, False) + else: + assert False, "Implementation error: not yet implemented" + + # Not following symlinks to files is not yet supported: reset to True +# if not curval.file: +# curval = FollowSymlinkConfig( +# curval.command_line, curval.directory, True) +# logging.warning("Coercing options to `follow-file-symlinks'") + setattr(namespace, self.dest, curval) + + +def gen_generate_opts(directories=[], + algorithm=util.default_algotag(), + append_output=False, + base64=False, + comment=[], + follow_symlinks=FollowSymlinkConfig(False, False, False), + full_mode=False, + generator="normal", + logical=None, + minimal=None, + mode=False, + mmap=None, + mtime=False, + output=None, + print_size=False, + size_only=False, + utf8=False): + if not isinstance(follow_symlinks, FollowSymlinkConfig): + raise TypeError("`follow_symlinks' must be a FollowSymlinkConfig") + # Not following symlinks to files is not yet supported: reset to True +# if not follow_symlinks.file: +# follow_symlinks = follow_symlinks._make([follow_symlinks.command_line, +# follow_symlinks.directory, +# True]) +# logging.warning("Coercing to follow-symlinks-file") + opts = argparse.Namespace( + directories=directories, + algorithm=util.argv2algo(algorithm), + append_output=append_output, + base64=base64, + comment=comment, + follow_symlinks=follow_symlinks, + generator=generator, + logical=logical, + minimal=minimal, + mmap=mmap, + metadata_full_mode=full_mode, + metadata_mode=mode, + metadata_mtime=mtime, + output=output, + print_size=print_size, + size_only=size_only, + utf8=utf8) + return opts + + +def gen_info_opts(digest_files=[], last=False): + opts = argparse.Namespace( + digest_files=digest_files, + print_only_last_block=last) + return opts + + +def treesum(opts): + # XXX TBD: opts.check and opts.checklist (as in shasum.py) + if opts.subcommand in ("generate", "gen"): + return generate_treesum(opts) + elif opts.subcommand == "info": + return print_treesum_digestfile_infos(opts) + else: + raise RuntimeError( + "command `{}' not yet handled".format(opts.subcommand)) + + +def generate_treesum(opts): + # Provide defaults + if not opts.algorithm: + opts.algorithm = util.argv2algo(util.default_algotag()) + if not opts.directories: + opts.directories.append(".") + + if opts.output is None or opts.output == "-": + if hasattr(sys.stdout, "buffer"): + out_cm = cm.nullcontext(sys.stdout.buffer) + else: + out_cm = cm.nullcontext(sys.stdout) + else: + if opts.append_output: + 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: + V1DirectoryTreesumGenerator( + opts.algorithm, opts.mmap, opts.base64, + opts.follow_symlinks, + opts.generator, + opts.metadata_mode, + opts.metadata_full_mode, + opts.metadata_mtime, + opts.size_only, + opts.print_size, + opts.utf8, + minimal=opts.minimal).generate( + outfp, d, comment=opts.comment) + + +class V1DirectoryTreesumGenerator(object): + + def __init__(self, algorithm, use_mmap, use_base64, + follow_symlinks, + with_generator, + with_metadata_mode, with_metadata_full_mode, + with_metadata_mtime, size_only, print_size, utf8_mode, + minimal=None,): + super(V1DirectoryTreesumGenerator, self).__init__() + self._algorithm = algorithm + self._use_mmap = use_mmap + self._use_base64 = use_base64 + self._follow_symlinks = follow_symlinks + self._with_generator = with_generator + self._with_metadata_mode = with_metadata_mode + self._with_metadata_full_mode = with_metadata_full_mode + self._with_metadata_mtime = with_metadata_mtime + self._size_only = size_only + self._print_size = print_size + self._utf8_mode = utf8_mode + self._minimal = minimal + + def generate(self, outfp, root, comment=None): + """ + + :param outfp: a *binary* file with a "write()" and a "flush()" method + + """ + self._outfp = outfp + self._outfp.resetdigest() + self._outfp.write(format_bsd_line("VERSION", "1", None, False)) + self._outfp.write(format_bsd_line( + "FSENCODING", util.n(walk.getfsencoding().upper()), None, False)) + self._outfp.flush() + + if self._with_generator == "none": + pass # do nothing + elif self._with_generator == "normal": + self._outfp.write(format_bsd_line( + "GENERATOR", None, b"PY2" if util.PY2 else b"PY3", False)) + elif self._with_generator == "full": + import platform + info = "%s %s, %s" % (platform.python_implementation(), + platform.python_version(), + platform.platform()) + self._outfp.write(format_bsd_line( + "GENERATOR", None, info.encode("utf-8"), False)) + else: + raise NotImplementedError( + "not implemented: %s" % (self._with_generator,)) + + # + # Note: Given non-default flags that are relevant for + # directory traversal. + # + flags = [] + if self._with_metadata_full_mode: + flags.append("with-metadata-fullmode") + elif self._with_metadata_mode: + flags.append("with-metadata-mode") + if self._with_metadata_mtime: + flags.append("with-metadata-mtime") + flags.append("follow-symlinks-commandline" + if self._follow_symlinks.command_line + else "no-follow-symlinks-commandline") + flags.append("follow-symlinks-directory" + if self._follow_symlinks.directory + else "no-follow-symlinks-directory") + flags.append("follow-symlinks-file" + if self._follow_symlinks.file + else "no-follow-symlinks-file") + if self._size_only: + flags.append("size-only") + flags.append("utf8-encoding" if self._utf8_mode else "fs-encoding") + if self._print_size: + flags.append("print-size") + flags.sort() + self._outfp.write( + format_bsd_line("FLAGS", ",".join(flags), None, False)) + + if self._minimal is None: + # Write execution timestamps in POSIX epoch and ISO format + ts = int(time.time()) + self._outfp.write(format_bsd_line("TIMESTAMP", ts, None, False)) + ts = (datetime.datetime.utcfromtimestamp(ts)).isoformat("T") + self._outfp.write(format_bsd_line("ISOTIMESTAMP", ts, None, False)) + + if comment: + for line in comment: + self._outfp.write( + format_bsd_line("COMMENT", None, line, False)) + + if self._minimal is not None: + self._outfp.write(format_bsd_line( + "ROOT", + None, + (walk.WalkDirEntry.alt_u8(self._minimal) + if self._minimal else b""), + False)) + else: + self._outfp.write(format_bsd_line( + "ROOT", None, walk.WalkDirEntry.alt_u8(root), False)) + self._outfp.flush() + + if not self._follow_symlinks.command_line and os.path.islink(root): + linktgt = walk.WalkDirEntry.from_readlink(os.readlink(root)) + linkdgst = self._algorithm[0]() + linkdgst.update( + util.interpolate_bytes( + b"%d:%s,", len(linktgt.fspath), linktgt.fspath)) + dir_dgst = self._algorithm[0]() + dir_dgst.update(b"1:L,") + dir_dgst.update( + util.interpolate_bytes( + b"%d:%s,", len(linkdgst.digest()), linkdgst.digest())) + if self._size_only: + self._outfp.write( + format_bsd_line( + "SIZE", + None, + "./@/", + False, + 0)) + else: + self._outfp.write( + format_bsd_line( + self._algorithm[1], + dir_dgst.digest(), + "./@/", + 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) + path = os.path.join(root, *top) if top else root + with walk.ScanDir(path) as dirscan: + fsobjects = list(dirscan) + if self._utf8_mode: + fsobjects.sort(key=walk.WalkDirEntry.sort_key_u8) + else: + fsobjects.sort(key=walk.WalkDirEntry.sort_key_fs) + dir_dgst = self._algorithm[0]() + dir_size = 0 + dir_tainted = False + for fso in fsobjects: + if fso.is_dir: + if fso.is_symlink and not self._follow_symlinks.directory: + linktgt = walk.WalkDirEntry.from_readlink( + os.readlink(fso.path)) + # linktgt = util.fsencode(os.readlink(fso.path))) + linkdgst = self._algorithm[0]() + if self._utf8_mode: + if linktgt.u8path is None: + dir_tainted = True + linkdgst.update(util.interpolate_bytes( + b"%d:%s,", + len(linktgt.alt_u8path), + linktgt.alt_u8path)) + else: + linkdgst.update(util.interpolate_bytes( + b"%d:%s,", + len(linktgt.u8path), + linktgt.u8path)) + if fso.u8name is None: + dir_tainted = True + dir_dgst.update(util.interpolate_bytes( + b"1:S,%d:%s,", + len(fso.alt_u8name), + fso.alt_u8name)) + else: + dir_dgst.update(util.interpolate_bytes( + b"1:S,%d:%s,", len(fso.u8name), fso.u8name)) + else: + if linktgt.fspath is None: + dir_tainted = True + linkdgst.update(util.interpolate_bytes( + b"%d:%s,", + len(linktgt.alt_fspath), + linktgt.alt_fspath)) + else: + linkdgst.update(util.interpolate_bytes( + b"%d:%s,", + len(linktgt.fspath), + linktgt.fspath)) + if fso.fsname is None: + dir_tainted = True + dir_dgst.update(util.interpolate_bytes( + b"1:S,%d:%s,", + len(fso.alt_fsname), + fso.alt_fsname)) + else: + dir_dgst.update(util.interpolate_bytes( + b"1:S,%d:%s,", len(fso.fsname), fso.fsname)) + # + # - no mtime and no mode for symlinks + # - also does not count for dir_size + # + dir_dgst.update(util.interpolate_bytes( + b"%d:%s,", + len(linkdgst.digest()), linkdgst.digest())) + opath = join_output_path(top, fso.name) + if self._utf8_mode: + opath = walk.WalkDirEntry.alt_u8(opath) + else: + opath = walk.WalkDirEntry.alt_fs(opath) + if self._size_only: + self._outfp.write(format_bsd_line( + "SIZE", None, + util.interpolate_bytes(b"%s/./@/", opath), + False, 0)) + else: + self._outfp.write(format_bsd_line( + self._algorithm[1], + linkdgst.digest(), + util.interpolate_bytes(b"%s/./@/", opath), + self._use_base64)) + self._outfp.flush() + else: + # + # Follow the symlink to dir or handle a "real" directory + # + + # Get subdir data from recursing into it + sub_dir_dgst, sub_dir_size = self._generate( + root, top + (fso.name, )) + + dir_size += sub_dir_size + if self._utf8_mode: + if fso.u8name is None: + dir_tainted = True + dir_dgst.update(util.interpolate_bytes( + b"1:d,%d:%s,", + len(fso.alt_u8name), + fso.alt_u8name)) + else: + dir_dgst.update(util.interpolate_bytes( + b"1:d,%d:%s,", len(fso.u8name), fso.u8name)) + else: + if fso.fsname is None: + dir_tainted = True + dir_dgst.update(util.interpolate_bytes( + b"1:d,%d:%s,", + len(fso.alt_fsname), + fso.alt_fsname)) + else: + dir_dgst.update(util.interpolate_bytes( + b"1:d,%d:%s,", len(fso.fsname), fso.fsname)) + dir_dgst.update(util.interpolate_bytes( + b"%d:%s,", len(sub_dir_dgst), sub_dir_dgst)) + if self._with_metadata_full_mode: + modestr = util.b(normalized_mode_str(fso.stat.st_mode)) + dir_dgst.update(util.interpolate_bytes( + b"8:fullmode,%d:%s,", len(modestr), modestr)) + elif self._with_metadata_mode: + modestr = util.b(normalized_compatible_mode_str( + fso.stat.st_mode)) + dir_dgst.update(util.interpolate_bytes( + b"4:mode,%d:%s,", len(modestr), modestr)) + else: + if fso.is_symlink and not self._follow_symlinks.file: + linktgt = walk.WalkDirEntry.from_readlink( + os.readlink(fso.path)) + # linktgt = util.fsencode(os.readlink(fso.path))) + linkdgst = self._algorithm[0]() + if self._utf8_mode: + if linktgt.u8path is None: + dir_tainted = True + linkdgst.update(util.interpolate_bytes( + b"%d:%s,", + len(linktgt.alt_u8path), + linktgt.alt_u8path)) + else: + linkdgst.update(util.interpolate_bytes( + b"%d:%s,", + len(linktgt.u8path), + linktgt.u8path)) + if fso.u8name is None: + dir_tainted = True + dir_dgst.update(util.interpolate_bytes( + b"1:F,%d:%s,", + len(fso.alt_u8name), + fso.alt_u8name)) + else: + dir_dgst.update(util.interpolate_bytes( + b"1:F,%d:%s,", len(fso.u8name), fso.u8name)) + else: + if linktgt.fspath is None: + dir_tainted = True + linkdgst.update(util.interpolate_bytes( + b"%d:%s,", + len(linktgt.alt_fspath), + linktgt.alt_fspath)) + else: + linkdgst.update(util.interpolate_bytes( + b"%d:%s,", + len(linktgt.fspath), + linktgt.fspath)) + if fso.fsname is None: + dir_tainted = True + dir_dgst.update(util.interpolate_bytes( + b"1:F,%d:%s,", + len(fso.alt_fsname), + fso.alt_fsname)) + else: + dir_dgst.update(util.interpolate_bytes( + b"1:F,%d:%s,", len(fso.fsname), fso.fsname)) + # + # - no mtime and no mode for symlinks + # - also does not count for dir_size + # + dir_dgst.update(util.interpolate_bytes( + b"%d:%s,", + len(linkdgst.digest()), linkdgst.digest())) + opath = join_output_path(top, fso.name) + if self._utf8_mode: + opath = walk.WalkDirEntry.alt_u8(opath) + else: + opath = walk.WalkDirEntry.alt_fs(opath) + if self._size_only: + self._outfp.write(format_bsd_line( + "SIZE", None, + util.interpolate_bytes(b"%s/./@", opath), + False, 0)) + else: + self._outfp.write(format_bsd_line( + self._algorithm[1], + linkdgst.digest(), + util.interpolate_bytes(b"%s/./@", opath), + self._use_base64)) + self._outfp.flush() + else: + # + # Follow the symlink to file or handle a "real" file + # + + if self._utf8_mode: + if fso.u8name is None: + dir_tainted = True + dir_dgst.update(util.interpolate_bytes( + b"1:f,%d:%s,", + len(fso.alt_u8name), + fso.alt_u8name)) + else: + dir_dgst.update(util.interpolate_bytes( + b"1:f,%d:%s,", len(fso.u8name), fso.u8name)) + else: + if fso.fsname is None: + dir_tainted = True + dir_dgst.update(util.interpolate_bytes( + b"1:f,%d:%s,", + len(fso.alt_fsname), + fso.alt_fsname)) + else: + dir_dgst.update(util.interpolate_bytes( + b"1:f,%d:%s,", len(fso.fsname), fso.fsname)) + dir_size += fso.stat.st_size + if self._with_metadata_mtime: + mtime = datetime.datetime.utcfromtimestamp( + int(fso.stat.st_mtime)) + mtime = util.b(mtime.isoformat("T") + "Z") + dir_dgst.update(util.interpolate_bytes( + b"5:mtime,%d:%s,", len(mtime), mtime)) + if self._with_metadata_full_mode: + modestr = util.b(normalized_mode_str(fso.stat.st_mode)) + dir_dgst.update(util.interpolate_bytes( + b"8:fullmode,%d:%s,", len(modestr), modestr)) + elif self._with_metadata_mode: + modestr = util.b(normalized_compatible_mode_str( + fso.stat.st_mode)) + dir_dgst.update(util.interpolate_bytes( + b"4:mode,%d:%s,", len(modestr), modestr)) + if not self._size_only: + dgst = digest.compute_digest_file( + self._algorithm[0], + fso.path, + use_mmap=self._use_mmap) + dir_dgst.update(util.interpolate_bytes( + b"%d:%s,", len(dgst), dgst)) + opath = join_output_path(top, fso.name) + if self._utf8_mode: + opath = walk.WalkDirEntry.alt_u8(opath) + else: + opath = walk.WalkDirEntry.alt_fs(opath) + if self._size_only: + self._outfp.write(format_bsd_line( + "SIZE", None, opath, False, fso.stat.st_size)) + else: + if self._print_size: + self._outfp.write(format_bsd_line( + self._algorithm[1], + dgst, opath, + self._use_base64, + fso.stat.st_size)) + else: + self._outfp.write(format_bsd_line( + self._algorithm[1], dgst, opath, + self._use_base64)) + self._outfp.flush() + opath = join_output_path(top, None) + if opath: + if self._utf8_mode: + opath = walk.WalkDirEntry.alt_u8(opath) + else: + opath = walk.WalkDirEntry.alt_fs(opath) + if self._size_only: + self._outfp.write(format_bsd_line( + "SIZE", None, opath, False, dir_size)) + else: + if dir_tainted: + # + # IMPORTANT: Print errors BEFORE the associated digest line. + # Otherwise the "info" command has a problem. + # + self._outfp.write(format_bsd_line( + b"ERROR", None, b"directory is tainted", False, None)) + logging.error("Directory has filename problems: %r", opath) + if self._print_size: + self._outfp.write(format_bsd_line( + self._algorithm[1], dir_dgst.digest(), opath, + self._use_base64, dir_size)) + else: + self._outfp.write(format_bsd_line( + self._algorithm[1], dir_dgst.digest(), opath, + self._use_base64)) + self._outfp.flush() + return (dir_dgst.digest(), dir_size) + + +def join_output_path(top, name): + if name is None: + # a path for a directory is to be computed + if top: + if isinstance(top[0], bytes): + return b"/".join(top) + b"/" + else: + return u"/".join(top) + u"/" + else: + return b"" + else: + # a path for a normal file is to be computed + if top: + if isinstance(name, bytes): + return b"/".join(top) + b"/" + name + else: + return u"/".join(top) + u"/" + name + else: + return name + + +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.crcdigest())[2:]).upper() + + def crcdigest(self): + """ + + :rtype: int + + """ + if util.PY2: + if self._crc32 < 0: + # Return the bitpattern as unsigned 32-bit number + return (~self._crc32 ^ 0xFFFFFFFF) + else: + return self._crc32 + else: + return self._crc32 + + +def normalized_compatible_mode_str(mode): + # XXX FIXME: Windows and "executable" + modebits = stat.S_IMODE(mode) + modestr = "%o" % (modebits,) + if not modestr.startswith("0"): + modestr = "0" + modestr + return modestr + + +def normalized_mode_str(mode): + modestr = "%o" % (mode,) + if not modestr.startswith("0"): + modestr = "0" + modestr + return modestr + + +def format_bsd_line(what, value, filename, use_base64, size=None): + ls = util.b(os.linesep) + if not isinstance(what, bytes): + what = what.encode("ascii") + if what == b"TIMESTAMP": + assert filename is None + return util.interpolate_bytes(b"TIMESTAMP = %d%s", value, ls) + if what in (b"FSENCODING", b"ISOTIMESTAMP", b"FLAGS", b"VERSION", + b"CRC32"): + assert filename is None + return util.interpolate_bytes(b"%s = %s%s", what, util.b(value), ls) + assert filename is not None + if what in (b"COMMENT", b"ERROR", b"GENERATOR"): + return util.interpolate_bytes( + b"%s (%s)%s", what, util.b(filename, "utf-8"), ls) + if not isinstance(filename, bytes): + filename = util.fsencode(filename) + if what == b"SIZE": + return util.interpolate_bytes(b"SIZE (%s) = %d%s", filename, size, ls) + if value is None: + return util.interpolate_bytes(b"%s (%s)%s", what, filename, ls) + if use_base64: + value = base64.b64encode(value) + else: + value = binascii.hexlify(value) + if filename != b"./@/": + filename = util.normalize_filename(filename, True) + if size is None: + return util.interpolate_bytes( + b"%s (%s) = %s%s", what, filename, value, ls) + else: + return util.interpolate_bytes( + b"%s (%s) = %s,%d%s", what, filename, value, size, ls) + + +class TreesumReader(object): + + """Reader to read and/or verify treesum digest files. + + Supports the iterator and context manager protocol. + + """ + + PATTERN0 = re.compile(br"\A[ \t]*\r?\n\Z") # empty lines + PATTERN1 = re.compile(br"\A(VERSION|FSENCODING|FLAGS|TIMESTAMP|ISOTIMESTAMP|CRC32)[ \t]*=[ \t]*([^ \t]+)[ \t]*\r?\n\Z") # noqa: E501 line too long + PATTERN2 = re.compile(br"\A(ROOT|COMMENT|ERROR|GENERATOR)[ \t]*\((.*)\)[ \t]*\r?\n\Z") # noqa: E501 line too long + PATTERN3 = re.compile(br"\ASIZE[ \t]*\((.*)\)[ \t]*=[ \t]*(\d+)[ \t]*\r?\n\Z") # noqa: E501 line too long + PATTERN4 = re.compile(br"\A([A-Za-z0-9_-]+)[ \t]*\((.*)\)[ \t]*=[ \t]*([A-Za-z0-9=+/]+)(,(\d+))?[ \t]*\r?\n\Z") # noqa: E501 line too long + + def __init__(self, _fp, _filename, _own_fp): + self._fp = _fp + self._own_fp = _own_fp + self._filename = _filename + self._line_no = 0 + self._reset_crc() + self._expect_crc = None # NOTE: tristate: None is different from False + self._current_algo_name = self._current_algo_digest_size = None + + @classmethod + def from_path(cls_, path): + """Open file at `path` and return a reader that owns the file object""" + return cls_(open(path, "rb"), path, True) + + @classmethod + def from_binary_buffer(cls_, binary_fp, filename): + return cls_(binary_fp, filename, False) + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + def close(self): + if self._fp is not None: + try: + if self._own_fp: + self._fp.close() + finally: + self._fp = None + + def __iter__(self): + return self + + def __next__(self): + rec = self.read_record() + if rec is None: + raise StopIteration() + return rec + + if util.PY2: + next = __next__ + + def all_records(self): + """Iterator over all remaining records""" + while True: + rec = self.read_record() + if rec is None: + return + yield rec + + def read_record(self): + """Read and parse the "next" line. + + :returns: `None` at EOF or the parsed contents of the line + :rtype: tuple or None + + """ + # Loop to skip empty lines + while True: + line = self._get_next_line() + if not line: + # + # Skip for empty files at the very beginning. + # Check only after the first VERSION line. + # + if self._expect_crc is not None: + if self._expect_crc: + logging.warning("CRC32 is missing at EOF") + return None + if not self.PATTERN0.search(line): + break + self._update_crc(line) + # + # At the beginning transparently skip an eventually embedded signify + # signature + # + if self._line_no == 1: + if line.startswith(b"untrusted comment: "): + line = self._get_next_line() + if not line.endswith(b"\n"): + raise binascii.Error("No valid signify signature value") + # Try to decode for an early error check + base64.b64decode(line[:-1]) + mo = self.PATTERN1.search(line) + if mo: + if mo.group(1) == b"VERSION": + if self._expect_crc: + logging.warning("CRC32 missing before line %d", + self._line_no) + self._reset_crc() + self._expect_crc = True + self._update_crc(line) + return ("VERSION", util.n(mo.group(2))) + if mo.group(1) == b"CRC32": + # TODO: check + if self._expect_crc is None: + logging.warning("Lone CRC32 before VERSION in line %d", + self._line_no) + else: + if self._expect_crc: + if (self._hex_crc() + != mo.group(2).decode("latin1").upper()): + logging.warning( + "CRC32 mismatch in line %d:" + " expected: %s, given: %s", + self._line_no, + self._hex_crc(), + mo.group(2).decode("latin1").upper()) + else: + logging.warning("CRC32 before VERSION in line %d", + self._line_no) + # Do not update the CRC here but reset the state + self._expect_crc = False + return ("CRC32", util.n(mo.group(2))) + else: + self._update_crc(line) + return (util.n(mo.group(1)), util.n(mo.group(2))) + else: + mo = self.PATTERN2.search(line) + if mo: + self._update_crc(line) + if mo.group(1) in (b"COMMENT", b"ERROR", b"GENERATOR"): + return (util.u(mo.group(1)), util.u(mo.group(2), "utf-8")) + elif mo.group(1) == b"ROOT": + return ("ROOT", mo.group(2)) + assert False, line + else: + mo = self.PATTERN3.search(line) + if mo: + self._update_crc(line) + return ("SIZE", mo.group(1), int(util.n(mo.group(2)), 10)) + else: + mo = self.PATTERN4.search(line) + if mo: + self._update_crc(line) + algo_name = util.n(mo.group(1)) + if (len(mo.group(3)) == + 2 * self._get_digest_size(algo_name)): + # hex + digest = binascii.unhexlify(mo.group(3)) + else: + # base64 + digest = base64.b64decode(mo.group(3)) + if mo.group(4): + size = int(util.n(mo.group(5)), 10) + else: + size = None + return (algo_name, mo.group(2), digest, size) + else: + assert False, line + return line + + def _get_next_line(self): + line = self._fp.readline(4096) # along PATH_MAX on Linux + if line: + self._line_no += 1 + return line + + def _reset_crc(self): + self._crc32 = zlib.crc32(b"") + + def _update_crc(self, data): + self._crc32 = zlib.crc32(data, self._crc32) + + def _hex_crc(self): + return (hex(self._get_crc())[2:]).upper() + + def _get_crc(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 util.PY2: + if self._crc32 < 0: + # Return the bitpattern as unsigned 32-bit number + return (~self._crc32 ^ 0xFFFFFFFF) + else: + return self._crc32 + else: + return self._crc32 + + def _get_digest_size(self, algo_name): + if self._current_algo_name == algo_name: + return self._current_algo_digest_size + h = util.algotag2algotype(algo_name)() + self._current_algo_name = algo_name + self._current_algo_digest_size = h.digest_size + return self._current_algo_digest_size + + +def print_treesum_digestfile_infos(opts): + print_infos_for_digestfile(opts.digest_files, opts.print_only_last_block) + + +def print_infos_for_digestfile(digest_files, print_only_last_block=True): + for fn in digest_files: + if fn == "-": + if util.PY2: + reader = TreesumReader.from_binary_buffer(sys.stdin) + else: + reader = TreesumReader.from_binary_buffer(sys.stdin.buffer) + else: + reader = TreesumReader.from_path(fn) + + with reader: + root = generator = flags = fsencoding = algorithm = digest \ + = size = None + errors = set() + comments = [] + in_block = False + block_no = 0 + for record in reader: + if record[0] == "VERSION": + assert record[1] == "1" + # start a new block + in_block = True + block_no += 1 + root = flags = algorithm = digest = size = None + comments = [] + elif record[0] == "GENERATOR": + generator = record[1] + elif record[0] == "FSENCODING": + fsencoding = record[1] + elif record[0] == "FLAGS": + flags = record[1] + elif record[0] == "ROOT": + root = record[1] + elif record[0] == "COMMENT": + comments.append(record[1]) + elif record[0] == "ERROR": + errors.add(record[1]) + elif record[0] in ("TIMESTAMP", "ISOTIMESTAMP"): + pass + elif record[0] == "CRC32": + pass + # in_block = False + else: + if not in_block: + continue + # digest line or size line + if not record[1] or record[1] == b"./@/": + if record[0] == "SIZE": + algorithm = "SIZE" + digest = None + size = record[2] + else: + algorithm = record[0] + digest = record[2] + size = record[3] + if not print_only_last_block: + print_block_data( + block_no, + root, generator, fsencoding, flags, comments, + errors, algorithm, digest, size) + root = generator = flags = fsencoding = algorithm \ + = digest = size = None + errors = set() + comments = [] + in_block = False + if print_only_last_block: + if not in_block: + if digest is not None or size is not None: + print_block_data( + block_no, + root, generator, fsencoding, flags, comments, errors, + algorithm, digest, size) + else: + logging.warning("missing block end") + + +def print_block_data(block_no, tag, generator, fsencoding, flags, comments, + errors, algorithm, digest, size): + digeststr = util.n(binascii.hexlify(digest)) if digest else "" + sizestr = str(size) if size is not None else "" + print("BLOCK No %d:" % (block_no,)) + print(" Tag:", tag) + print(" FS-Encoding:", fsencoding) + if generator: + print(" Generator:", generator) + print(" Flags:", flags if flags else "") + if comments: + print(" Comments:", comments) + print(" Algorithm:", algorithm) + if algorithm != "SIZE": + print(" Digest:", digeststr) + print(" Size:", sizestr) + print(" Errors:", errors if errors else "") + + +if __name__ == "__main__": + sys.exit(main()) diff -r 4bb2d0975cfe -r a828f320ac58 cutils/util/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cutils/util/__init__.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,325 @@ +# -*- coding: utf-8 -*- +# :- +# :Copyright: (c) 2020-2025 Franz Glasner +# :License: BSD-3-Clause +# :- +r"""Utility package. + +""" + +from __future__ import print_function, absolute_import + + +__all__ = ["PY2", + "PY35", + "n", "b", "u", + "normalize_filename", + "argv2algo", + "algotag2algotype", + "get_blake2b", + "get_blake2b_256", + "get_blake2s", + "default_algotag", + "fsencode", + "interpolate_bytes", + ] + + +import argparse +import hashlib +import os +import sys + + +PY2 = sys.version_info[0] < 3 +PY35 = sys.version_info[:2] >= (3, 5) + + +if PY2: + + def n(s, encoding="ascii"): + """Convert `s` to the native string implementation""" + if isinstance(s, unicode): # noqa: F821 undefined name 'unicode' + return s.encode(encoding) + return s + + def b(s, encoding="ascii"): + """Convert `s` to bytes""" + if isinstance(s, unicode): # noqa: F821 undefined name 'unicode' + return s.encode(encoding) + return s + + def u(s, encoding="ascii"): + """Convert `s` to a unicode string""" + if isinstance(s, str): + return s.decode(encoding) + return s + +else: + + def n(s, encoding="ascii"): + """Convert `s` to the native string implementation""" + if isinstance(s, (bytes, bytearray)): + return s.decode(encoding) + return s + + def b(s, encoding="ascii"): + """Convert `s` to bytes""" + if isinstance(s, str): + return s.encode(encoding) + return s + + u = n + + +def default_algotag(): + """Determine the "best" default algorithm. + + Depend on availability in :mod:`hashlib`. + + Prefer BLAKE2b-256, SHA256 or SHA1 -- in this order. + + Does not consider :mod:`pyblake2` if it is available eventually. + + """ + if "blake2b" in hashlib.algorithms_available: + return "BLAKE2b-256" + if "sha256" in hashlib.algorithms_available: + return "SHA256" + return "SHA1" + + +def get_blake2b(): + """Get the factory for blake2b""" + try: + return hashlib.blake2b + except AttributeError: + import pyblake2 + return pyblake2.blake2b + + +def get_blake2b_256(): + """Get the factory for blake2b-256""" + + try: + hashlib.blake2b + except AttributeError: + import pyblake2 + + def _get_blake(): + return pyblake2.blake2b(digest_size=32) + + else: + + def _get_blake(): + return hashlib.blake2b(digest_size=32) + + return _get_blake + + +def get_blake2s(): + """Get the factory for blake2s""" + try: + return hashlib.blake2s + except AttributeError: + import pyblake2 + return pyblake2.blake2s + + +def get_crc(name): + """Get the factory for a CRC""" + + from ..crcmod.predefined import PredefinedCrc + + def _crc_type(): + return PredefinedCrc(name) + + return _crc_type + + +def argv2algo(s): + """Convert a command line algorithm specifier into a tuple with the + type/factory of the digest and the algorithms tag for output purposes. + + :param str s: the specifier from the command line; should include all + algorithm tags also (for proper round-tripping) + :return: the internal digest specification + :rtype: a tuple (digest_type_or_factory, name_in_output) + :raises argparse.ArgumentTypeError: for unrecognized algorithms or names + + String comparisons are done case-insensitively. + + """ + s = s.lower() + if s in ("1", "sha1"): + return (hashlib.sha1, "SHA1") + elif s in ("224", "sha224"): + return (hashlib.sha224, "SHA224") + elif s in ("256", "sha256"): + return (hashlib.sha256, "SHA256") + elif s in ("384", "sha384"): + return (hashlib.sha384, "SHA384") + elif s in ("512", "sha512"): + return (hashlib.sha512, "SHA512") + elif s in ("3-224", "sha3-224"): + return (hashlib.sha3_224, "SHA3-224") + elif s in ("3-256", "sha3-256"): + return (hashlib.sha3_256, "SHA3-256") + elif s in ("3-384", "sha3-384"): + return (hashlib.sha3_384, "SHA3-384") + elif s in ("3", "3-512", "sha3-512"): + return (hashlib.sha3_512, "SHA3-512") + elif s in ("blake2b", "blake2b-512", "blake2", "blake2-512"): + return (get_blake2b(), "BLAKE2b") + elif s in ("blake2s", "blake2s-256"): + return (get_blake2s(), "BLAKE2s") + elif s in ("blake2-256", "blake2b-256"): + return (get_blake2b_256(), "BLAKE2b-256") + elif s == "md5": + return (hashlib.md5, "MD5") + elif s in ("crc24", "crc-24", + "crc24-openpgp", "crc-24-openpgp"): + return (get_crc("crc-24"), "CRC-24") + elif s in ("crc32", "crc-32", + "crc32-pkzip", "crc-32-pkzip", + "crc32-iso", "crc-32-iso", + "crc32-iso-hdlc", "crc-32-iso-hdlc"): + return (get_crc("crc-32"), "CRC-32-ISO") + elif s in ("crc32-posix", "crc-32-posix", + "crc32-cksum", "crc-32-cksum", + "posix"): + return (get_crc("posix"), "CRC-32-POSIX") + elif s in ("crc64", "crc-64", + "crc64-iso", "crc-64-iso"): + return (get_crc("crc-64"), "CRC-64-ISO") + elif s in ("crc64-2", "crc-64-2", + "crc64-iso-2", "crc-64-iso-2", + "crc64-mcrc64", "crc-64-mcrc64"): + return (get_crc("crc-64-2"), "CRC-64-ISO-2") + elif s in ("crc64-ecma", "crc-64-ecma"): + return (get_crc("crc-64-ecma"), "CRC-64-ECMA") + elif s in ("crc64-xz", "crc-64-xz", + "crc64-go-ecma", "crc-64-go-ecma"): + return (get_crc("crc-64-xz"), "CRC-64-XZ") + elif s in ("crc64-go", "crc-64-go", + "crc64-go-iso", "crc-64-go-iso"): + return (get_crc("crc-64-go"), "CRC-64-GO-ISO") + elif s in ("crc64-redis", "crc-64-redis"): + return (get_crc("crc-64-redis"), "CRC-64-REDIS") + else: + raise argparse.ArgumentTypeError( + "`{}' is not a recognized algorithm".format(s)) + + +def algotag2algotype(s): + """Convert the algorithm specifier in a BSD-style digest file to the + type/factory of the corresponding algorithm. + + :param str s: the tag (i.e. normalized name) or the algorithm + :return: the digest type or factory for `s` + :raises ValueError: on unknown and/or unhandled algorithms + + All string comparisons are case-sensitive. + + """ + if s == "SHA1": + return hashlib.sha1 + elif s == "SHA224": + return hashlib.sha224 + elif s == "SHA256": + return hashlib.sha256 + elif s == "SHA384": + return hashlib.sha384 + elif s == "SHA512": + return hashlib.sha512 + elif s == "SHA3-224": + return hashlib.sha3_224 + elif s == "SHA3-256": + return hashlib.sha3_256 + elif s == "SHA3-384": + return hashlib.sha3_384 + elif s == "SHA3-512": + return hashlib.sha3_512 + elif s in ("BLAKE2b", "BLAKE2b-512", "BLAKE2b512"): # compat for openssl + return get_blake2b() + elif s in ("BLAKE2s", "BLAKE2s-256", "BLAKE2s256"): # compat for openssl + return get_blake2s() + elif s in ("BLAKE2b-256", "BLAKE2b256"): # also compat for openssl dgst + return get_blake2b_256() + elif s == "MD5": + return hashlib.md5 + elif s == "CRC-24": + return get_crc("crc-24") + elif s == "CRC-32-ISO": + return get_crc("crc-32") + elif s == "CRC-32-POSIX": + return get_crc("posix") + elif s == "CRC-64-ISO": + return get_crc("crc-64") + elif s == "CRC-64-ISO-2": + return get_crc("crc-64-2") + elif s == "CRC-64-ECMA": + return get_crc("crc-64-ecma") + elif s == "CRC-64-XZ": + return get_crc("crc-64-xz") + elif s == "CRC-64-GO-ISO": + return get_crc("crc-64-go") + elif s == "CRC-64-REDIS": + return get_crc("crc-64-redis") + else: + raise ValueError("unknown algorithm: {}".format(s)) + + +def normalize_filename(filename, strip_leading_dot_slash=False): + if isinstance(filename, bytes): + filename = filename.replace(b"\\", b"/") + if strip_leading_dot_slash: + while filename.startswith(b"./"): + filename = filename[2:] + else: + filename = filename.replace(u"\\", u"/") + if strip_leading_dot_slash: + while filename.startswith(u"./"): + filename = filename[2:] + return filename + + +def fsencode(what): + """A somewhat compatibility function for :func:`os.fsencode`. + + If `what` is of type :class:`bytes` no :func:`os.fsencode` is required. + + """ + if isinstance(what, bytes): + return what + return os.fsencode(what) + + +def interpolate_bytes(formatstr, *values): + """Interpolate byte strings also on Python 3.4. + + :param bytes formatstr: + :param values: params for interpolation: may *not* contain Unicode strings + :rvalue: the formatted octet + :rtype: bytes + + """ + assert isinstance(formatstr, bytes) + # Python 3.5+ or Python2 know how to interpolate byte strings + if PY35 or PY2: + return formatstr % values + # Workaround with a Latin-1 dance + tformatstr = formatstr.decode("latin1") + tvalues = [] + for v in values: + if PY2: + if isinstance(v, unicode): # noqa: F821 undefined name 'unicode' + assert False + else: + if isinstance(v, str): + assert False + if isinstance(v, bytes): + tvalues.append(v.decode("latin1")) + else: + tvalues.append(v) + return (tformatstr % tuple(tvalues)).encode("latin1") diff -r 4bb2d0975cfe -r a828f320ac58 cutils/util/cm.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cutils/util/cm.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# :- +# :Copyright: (c) 2020-2025 Franz Glasner +# :License: BSD-3-Clause +# :- +r"""Context manager extensions and compatibility. + +""" + +from __future__ import print_function, absolute_import + + +__all__ = ["nullcontext"] + + +try: + from contextlib import nullcontext +except ImportError: + class nullcontext(object): + + """Compatibility implementation for systems that are missing yet + a standard :class:`contextlib.nullcontext`. + + """ + + __slots__ = ("thing", ) + + def __init__(self, thing=None): + self.thing = thing + + def __enter__(self): + return self.thing + + def __exit__(self, *args, **kwds): + pass diff -r 4bb2d0975cfe -r a828f320ac58 cutils/util/constants.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cutils/util/constants.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# :- +# :Copyright: (c) 2020-2025 Franz Glasner +# :License: BSD-3-Clause +# :- +r"""Common constants and compatibility definitions. + +""" + +from __future__ import print_function, absolute_import + + +__all__ = ["PATH_TYPES", + "READ_CHUNK_SIZE", + "MAX_AUTO_MAP_SIZE", + "MAP_WINDOW_SIZE" + ] + + +try: + import pathlib +except ImportError: + pathlib = None + +from . import PY2 + + +if PY2: + PATH_TYPES = (unicode, str) # noqa: F821 (undefined name 'unicode') +else: + if pathlib: + PATH_TYPES = (str, bytes, pathlib.Path) + else: + PATH_TYPES = (str, bytes) + +READ_CHUNK_SIZE = 2 * 1024 * 1024 # like BUFSIZE_MAX on FreeBSD +MAX_AUTO_MAP_SIZE = 8 * 1024 * 1024 +MAP_WINDOW_SIZE = MAX_AUTO_MAP_SIZE # do not totally trash memory on big files diff -r 4bb2d0975cfe -r a828f320ac58 cutils/util/digest.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cutils/util/digest.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +# :- +# :Copyright: (c) 2020-2025 Franz Glasner +# :License: BSD-3-Clause +# :- +r"""Utility sub-module to implement a file and stream digest computations. + +""" + +from __future__ import print_function, absolute_import + + +__all__ = ["compute_digest_file", "compute_digest_stream"] + + +import errno +import io +import os +try: + import mmap +except ImportError: + mmap = None +import stat + +from . import PY2 +from . import constants + + +def compute_digest_file(hashobj, path, use_mmap=None): + """Compute the digest for a file with a filename of an open fd. + + :param hashobj: a :mod:`hashlib` compatible hash algorithm type or factory + :param path: filename within the filesystem or a file descriptor opened in + binary mode (also a socket or pipe) + :param use_mmap: Use the :mod:`mmap` module if available. + If `None` determine automatically. + :type use_mmap: bool or None + :return: the digest in binary form + :rtype: bytes + + If a file descriptor is given is must support :func:`os.read`. + + """ + h = hashobj() + if isinstance(path, constants.PATH_TYPES): + flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) \ + | getattr(os, "O_SEQUENTIAL", 0) | getattr(os, "O_NOCTTY", 0) + fd = os.open(path, flags) + own_fd = True + else: + fd = path + own_fd = False + try: + try: + st = os.fstat(fd) + except TypeError: + # + # "fd" is most probably a Python socket object. + # (a pipe typically supports fstat) + # + use_mmap = False + else: + if stat.S_ISREG(st[stat.ST_MODE]): + filesize = st[stat.ST_SIZE] + if (use_mmap is None) \ + and (filesize > constants.MAX_AUTO_MAP_SIZE): + # + # This is borrowed from FreeBSD's cp(1) implementation: + # Mmap and process if less than 8M (the limit is + # so we don't totally trash memory on big files. + # This is really a minor hack, but it wins some + # CPU back. Some filesystems, such as smbnetfs, + # don't support mmap, so this is a best-effort + # attempt. + # + use_mmap = False + else: + use_mmap = False + if use_mmap is None: + use_mmap = True + if mmap is None or not use_mmap: + # No mmap available or wanted -> use traditional low-level file IO + fadvise = getattr(os, "posix_fadvise", None) + if fadvise: + fadvise(fd, 0, 0, os.POSIX_FADV_SEQUENTIAL) + if not PY2: + fileobj = io.FileIO(fd, mode="r", closefd=False) + buf = bytearray(constants.READ_CHUNK_SIZE) + with memoryview(buf) as full_view: + while True: + try: + n = fileobj.readinto(buf) + except OSError as e: + if e.errno not in (errno.EAGAIN, + errno.EWOULDBLOCK, + errno.EINTR): + raise + else: + if n == 0: + break + if n == constants.READ_CHUNK_SIZE: + h.update(buf) + else: + with full_view[:n] as partial_view: + h.update(partial_view) + else: + while True: + try: + buf = os.read(fd, constants.READ_CHUNK_SIZE) + except OSError as e: + if e.errno not in (errno.EAGAIN, + errno.EWOULDBLOCK, + errno.EINTR): + raise + else: + if len(buf) == 0: + break + h.update(buf) + else: + # + # Use mmap + # + # NOTE: On Windows mmapped files with length 0 are not supported. + # So ensure to not call mmap.mmap() if the file size is 0. + # + madvise = getattr(mmap.mmap, "madvise", None) + if filesize <= constants.MAP_WINDOW_SIZE: + mapsize = filesize + else: + mapsize = constants.MAP_WINDOW_SIZE + mapoffset = 0 + rest = filesize + while rest > 0: + m = mmap.mmap(fd, + mapsize, + access=mmap.ACCESS_READ, + offset=mapoffset) + if madvise: + madvise(m, mmap.MADV_SEQUENTIAL) + try: + h.update(m) + finally: + m.close() + rest -= mapsize + mapoffset += mapsize + if rest < mapsize: + mapsize = rest + finally: + if own_fd: + os.close(fd) + return h.digest() + + +def compute_digest_stream(hashobj, instream): + """Compute the digest for a given byte string `instream`. + + :param hashobj: a :mod:`hashlib` compatible hash algorithm type or factory + :param instream: a bytes input stream to read the data to be hashed from + :return: the digest in binary form + :rtype: bytes + + """ + h = hashobj() + while True: + try: + buf = instream.read(constants.READ_CHUNK_SIZE) + except OSError as e: + if e.errno not in (errno.EAGAIN, errno.EWOULDBLOCK, errno.EINTR): + raise + else: + if buf is not None: + if len(buf) == 0: + break + h.update(buf) + return h.digest() diff -r 4bb2d0975cfe -r a828f320ac58 cutils/util/walk.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cutils/util/walk.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,402 @@ +# -*- coding: utf-8 -*- +# :- +# :Copyright: (c) 2020-2025 Franz Glasner +# :License: BSD-3-Clause +# :- +r"""Utility sub-module to implement a heavily customized :func:`os.walk`. + +""" + +from __future__ import print_function, absolute_import + + +__all__ = ["WalkDirEntry", "ScanDir", "getfsencoding"] + + +import os +try: + from os import scandir +except ImportError: + try: + from scandir import scandir + except ImportError: + scandir = None +import sys + +from . import PY2 + + +_notset = object() + + +_FSENCODING = sys.getfilesystemencoding() + + +if PY2: + + def _unix_path(s): + if isinstance(s, bytes): + return s.replace(b"\\", b"/") + return s.replace(u"\\", u"/") + +else: + + def _unix_path(s): + return s.replace("\\", "/") + + +class WalkDirEntry(object): + + """A :class:`os.DirEntry` alike to be used in :func:`walk` and for + its results. + + """ + + __slots__ = ("_name", "_path", # encoded as given in the ctor + "_is_symlink", "_is_dir", "_stat_result", + "_alt_fsname", "_alt_u8name") + + def __init__(self, name, path): + self._name = name # the name as given in the constructor + """The name exactly as given in the ctor""" + self._path = _unix_path(path) + """The path as given in the ctor -- but normalized to have slashes""" + self._is_symlink = self._is_dir = self._stat_result = None + self._alt_fsname = self._alt_u8name = _notset + + @property + def name(self): + """The original name exactly as given in the ctor""" + return self._name + + @property + def path(self): + """The original path exactly as given in the ctor.""" + return self._path + + @property + def fsname(self): + """The name as bytes for the filesystem. + + :rtype: bytes or None + + """ + if PY2: + if isinstance(self._name, bytes): + return self._name + try: + return self._name.encode(_FSENCODING, "strict") + except UnicodeError: + return None + else: + return os.fsencode(self._name) + + @property + def alt_fsname(self): + """Alternative and "escaped" filesystem name -- always bytes. + + :rtype: bytes + + """ + if self._alt_fsname is _notset: + self._alt_fsname = WalkDirEntry.alt_fs(self._name) + return self._alt_fsname + + @property + def fspath(self): + """Always bytes. + + :rtype: bytes or None + + """ + if PY2: + if isinstance(self._path, bytes): + return self._path + try: + return self._path.encode(_FSENCODING, "strict") + except UnicodeError: + return None + else: + return os.fsencode(self._path) + + @property + def alt_fspath(self): + """Alternative and "escaped" filesystem path -- always bytes. + + :rtype: bytes + + """ + return WalkDirEntry.alt_fs(self._path) + + @staticmethod + def alt_fs(what): + if PY2: + if isinstance(what, bytes): + return what + return what.encode(_FSENCODING, "backslashreplace") + else: + return os.fsencode(what) + + @property + def uname(self): + """Always "real", strictly encoded Unicode or `None` if this is not + possible. + + :rtype: text or None + + """ + if PY2: + if isinstance(self._name, bytes): + try: + return self._name.decode(_FSENCODING, "strict") + except UnicodeError: + return None + else: + return self._name + else: + try: + self._name.encode("utf-8", "strict") + except UnicodeError: + return None + return self._name + + @property + def upath(self): + """Always "real", strictly encoded Unicode or `None` if this is not + possible. + + :rtype: text or None + + """ + if PY2: + if isinstance(self._path, bytes): + try: + return self._path.decode(_FSENCODING, "strict") + except UnicodeError: + return None + else: + return self._path + else: + try: + self._path.encode("utf-8", "strict") + except UnicodeError: + return None + return self._path + + @property + def u8name(self): + """`.uname` as UTF-8 or `None` (as strict as `uname`)""" + n = self.uname + return n if n is None else n.encode("utf-8", "strict") + + @property + def u8path(self): + """`.upath` as UTF-8 or `None` (as strict as `upath`""" + p = self.upath + return p if p is None else p.encode("utf-8", "strict") + + @property + def alt_u8name(self): + if self._alt_u8name is _notset: + self._alt_u8name = WalkDirEntry.alt_u8(self._name) + return self._alt_u8name + + @property + def alt_u8path(self): + return WalkDirEntry.alt_u8(self._path) + + @staticmethod + def alt_u8(what): + if PY2: + if isinstance(what, bytes): + try: + return (what.decode(_FSENCODING, "strict") + .encode("utf-8", "strict")) + except UnicodeError: + return (WalkDirEntry.surrogate_decode(what) + .encode("ascii", "backslashreplace")) + else: + return what.encode("ascii", "backslashreplace") + else: + return what.encode("utf-8", "backslashreplace") + + @property + def is_symlink(self): + return self._is_symlink + + @property + def is_dir(self): + return self._is_dir + + @property + def stat(self): + return self._stat_result + + def __repr__(self): + tag = "" + if self._is_symlink: + tag += "l" + if self._is_dir: + tag += "d" + if tag: + return "" % (self._name, tag) + return "" % (self._name,) + + @classmethod + def from_direntry(cls_, entry): + w = cls_(entry.name, entry.path) + try: + w._is_dir = entry.is_dir(follow_symlinks=True) + except OSError: + # + # If is_dir() raises an OSError, consider that the entry + # is not a directory, same behaviour than os.path.isdir(). + # + w._is_dir = False + try: + w._is_symlink = entry.is_symlink() + except OSError: + # + # If is_symlink() raises an OSError, consider that the entry + # is not a symbolic link, same behaviour than os.path.islink(). + # + w._is_symlink = False + # Do not supress errors here and (consistently) follow symlinks + w._stat_result = entry.stat(follow_symlinks=True) + return w + + @classmethod + def from_path_name(cls_, path, name, _do_stat=True): + """`_nostat` is to be used only for testing purposes""" + w = cls_(name, os.path.join(path, name)) + try: + w._is_dir = os.path.isdir(w._path) + except OSError: + # + # If is_dir() raises an OSError, consider that the entry + # is not a directory, same behaviour than os.path.isdir(). + # + w._is_dir = False + try: + w._is_symlink = os.path.islink(w._path) + except OSError: + # + # If is_symlink() raises an OSError, consider that the entry + # is not a symbolic link, same behaviour than os.path.islink(). + # + w._is_symlink = False + if _do_stat: + w._stat_result = os.stat(w._path) + return w + + @classmethod + def from_readlink(cls_, path): + w = cls_(os.path.basename(path), path) + return w + + @staticmethod + def sort_key_fs(entry): + return entry.alt_fsname # because it should never throw + + @staticmethod + def sort_key_u8(entry): + return entry.alt_u8name # because it should never throw + + if PY2: + + @staticmethod + def surrogate_decode(what): + """Decode the bytes object `what` using surrogates from :pep:`383` + for all non-ASCII octets. + + """ + uwhat = [] + assert isinstance(what, bytes) + for ch in what: + chcode = ord(ch) + if chcode <= 0x7f: + uwhat.append(unichr(chcode)) # noqa: F821 unichr + else: + uwhat.append(unichr(0xDC00 + chcode)) # noqa: F821 unichr + return u"".join(uwhat) + + +if scandir: + + class ScanDir(object): + + """An :func:`os.scandir` wrapper that is always an iterator and + a context manager. + + """ + + __slots__ = ("_scandir_it", ) + + def __init__(self, path): + super(ScanDir, self).__init__() + self._scandir_it = scandir(path) + + def __iter__(self): + return self + + def __next__(self): + if self._scandir_it is None: + raise StopIteration("closed") + return WalkDirEntry.from_direntry(next(self._scandir_it)) + + if PY2: + next = __next__ + + def __enter__(self): + return self + + def __exit__(self, *args, **kwds): + self.close() + + def close(self): + if self._scandir_it is not None: + if hasattr(self._scandir_it, "close"): + self._scandir_it.close() + self._scandir_it = None + +else: + + class ScanDir(object): + + """An :func:`os.scandir` wrapper that is always an iterator and + a context manager. + + """ + + __slots__ = ("_listdir_it", "_path") + + def __init__(self, path): + super(ScanDir, self).__init__() + self._listdir_it = iter(os.listdir(path)) + self._path = path + + def __iter__(self): + return self + + def __next__(self): + if self._listdir_it is None: + raise StopIteration("closed") + return WalkDirEntry.from_path_name(self._path, + next(self._listdir_it)) + + if PY2: + next = __next__ + + def __enter__(self): + return self + + def __exit__(self, *args, **kwds): + pass + + def close(self): + pass + + +def getfsencoding(): + """Return the stored _FSENCODING of this module""" + return _FSENCODING diff -r 4bb2d0975cfe -r a828f320ac58 docs/notes.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/notes.rst Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,81 @@ +.. -*- coding: utf-8; indent-tabs-mode: nil; -*- + + +In a aggregated directory checksum at the end of a block: +A path equals + + ``./@/`` + + Symlink to directory + + ``./@`` + + Symlink to other filesystem object + +Other paths that *end* with: + + ``/./@/`` + + Symlink to a directory + + ``/./@` + + Symlink to other filesystem object + + +Fields: + + ``FSENCODING`` + + The result of :func:`os.getfilesystemencoding` that is in effect when + generating treesum digests (process environment) + + ``ERROR`` + + Errors are reported: + + - For directories if the one of filenames has a filename problem + + ``FLAGS`` + + Some flags are always printed. + + - no file modes and no file mtime is taken into account for digesting + - no file size is printed + + Contains: + + - ``with-metadata-fullmode``: if a file mode is used completely as + given by the OS for digest computation + - ``with-metadata-mode``: if just the "portable" file modes are used + for digest computation + - ``with-metadata-mtime``: if the mtime as ISO-String and truncated to + seconds is used in digest computation + - ``print-size``: if the filesize is to to be printed also -- does not + change digests + - ``fs-encoding``: the filenames are given in FS encoding + - ``utf8-encoding``: the filenames are given in UTF-8 encoding + - ``follow-symlinks-XXX`` and ``no-follow-symlinkx-XXX``: if symlinks are followed on the command + line, while directory walking or for files + - ``size-only``: if no digest is to be computed and only a file's size + is printed and the accumulated file sizes for a directory tree + + ``ROOT`` + + Always printed in UTF-8 encoding + + ``COMMENT`` + + Always printed in UTF-8 encoding + + +Offene Fragen +============= + +VFAT und Encoding +----------------- + +Verfolgung von Encoding-Einstellungen über Mount-Punkte hinweg -- gerade +für externe Medien mit VFAT/MSDOS-FS. + + Erst einmal zurückgestellt diff -r 4bb2d0975cfe -r a828f320ac58 dos2unix.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dos2unix.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# :- +# :Copyright: (c) 2020-2025 Franz Glasner +# :License: BSD-3-Clause +# :- +r"""Pure Python implementation of `dos2unix`. + +""" + +from __future__ import absolute_import + +import sys + +import cutils.dos2unix + + +sys.exit(cutils.dos2unix.main()) diff -r 4bb2d0975cfe -r a828f320ac58 pyproject.toml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pyproject.toml Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,4 @@ +# Pure-Python-Build +[build-system] +requires = ["setuptools>=43.0", "wheel>=0.33"] +build-backend = "setuptools.build_meta" diff -r 4bb2d0975cfe -r a828f320ac58 setup.cfg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/setup.cfg Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,59 @@ +[egg_info] +tag_build = +tag_date = 0 +tag_svn_revision = 0 + +[bdist_wheel] +universal = 1 + +[sdist] +formats = gztar + +[metadata] +name = py-cutils +version = attr: cutils.__version__ +description = Pure Python implementation of some coreutils with some extensions +author = Franz Glasner +author_email = fzglas.hg@dom66.de +license = BSD 3-Clause "New" or "Revised" License +url = https://pypi.dom66.de/simple/py-cutils/ +download_url = https://pypi.dom66.de/simple/py-cutils/ +license_files = LICENSE.txt +long_description = file: README.txt +long_description_content_type = text/x-rst +platforms = any +classifiers = + Development Status :: 5 - Production/Stable + Environment :: Console + Intended Audience :: Developers + Intended Audience :: End Users/Desktop + Intended Audience :: System Administrators + License :: OSI Approved :: BSD License + Operating System :: OS Independent + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Topic :: System + Topic :: Utilities + +[options] +include_package_data = False +zip_safe = True +python_requires = >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.* +packages = + cutils + cutils.util + cutils.crcmod + cutils.crcmod.python2 + cutils.crcmod.python3 + +[options.entry_points] +console_scripts = + py-dos2unix = cutils.dos2unix:main + py-shasum = cutils.shasum:main + py-treesum = cutils.treesum:main + + +[flake8] +exclude = + # Ignore the vendored crcmod2/crcmod sub-package + cutils/crcmod diff -r 4bb2d0975cfe -r a828f320ac58 setup.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/setup.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from setuptools import setup + +setup() diff -r 4bb2d0975cfe -r a828f320ac58 shasum.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/shasum.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# :- +# :Copyright: (c) 2020-2025 Franz Glasner +# :License: BSD-3-Clause +# :- +r"""Pure Python implementation of `shasum`. + +""" + +from __future__ import absolute_import + +import sys + +import cutils.shasum + + +sys.exit(cutils.shasum.main()) diff -r 4bb2d0975cfe -r a828f320ac58 tests/_test_setup.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/_test_setup.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +r"""Unit tests + +""" + +import os +import sys + + +TESTDIR = os.path.normpath(os.path.abspath(os.path.dirname(__file__))) +DATADIR = os.path.join(TESTDIR, "data") + + +sys.path.insert(0, os.path.join(TESTDIR, "..")) + +del os +del sys diff -r 4bb2d0975cfe -r a828f320ac58 tests/data/empty diff -r 4bb2d0975cfe -r a828f320ac58 tests/test_crcmod.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_crcmod.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +r"""Unit tests for the vendored crcmod2 package. + +""" + +from __future__ import absolute_import, print_function + +import unittest + +import _test_setup + +import cutils.crcmod.test + + +if __name__ == "__main__": + unittest.main(module=cutils.crcmod.test) diff -r 4bb2d0975cfe -r a828f320ac58 tests/test_shasum.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_shasum.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +r"""Unit tests + +""" + +from __future__ import absolute_import, print_function + +import io +import os +import sys +import unittest +try: + from StringIO import StringIO +except ImportError: + StringIO = None + +from _test_setup import DATADIR + +from cutils import shasum + + +PY2 = sys.version_info[0] <= 2 + + +def _memfile(): + if StringIO: + return StringIO() + else: + return io.StringIO() + + +class ChangedDir(object): + + """Context manager to temporarily change the directory""" + + def __init__(self, path): + self._new_dir = path + self._prev_dir = None + + def __enter__(self): + self._prev_dir = os.getcwd() + os.chdir(self._new_dir) + + def __exit__(self, *args, **kwds): + if self._prev_dir is not None: + os.chdir(self._prev_dir) + self._prev_dir = None + + +class SignifyTests(unittest.TestCase): + + def test_empty(self): + destfile = _memfile() + opts = shasum.gen_opts(algorithm="SHA256", + dest=destfile, + files=[os.path.join(DATADIR, "empty")]) + shasum.shasum(opts) + self.assertTrue( + destfile.getvalue().startswith( + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 *")) + + def test_empty_with_name(self): + destfile = _memfile() + with ChangedDir(DATADIR): + opts = shasum.gen_opts(algorithm="SHA256", + dest=destfile, + files=["empty"]) + shasum.shasum(opts) + self.assertEqual( + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 *empty\n", + destfile.getvalue()) + + def test_empty_with_name_bsd(self): + destfile = _memfile() + with ChangedDir(DATADIR): + opts = shasum.gen_opts(algorithm="SHA512", + dest=destfile, + files=["empty"], + bsd=True) + shasum.shasum(opts) + self.assertEqual( + "SHA512 (empty) = cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e\n", + destfile.getvalue()) + + def test_empty_mmap(self): + destfile = _memfile() + opts = shasum.gen_opts(algorithm="SHA256", + dest=destfile, + files=[os.path.join(DATADIR, "empty")], + mmap=True) + shasum.shasum(opts) + self.assertTrue( + destfile.getvalue().startswith( + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 *")) + + def test_empty_no_mmap(self): + destfile = _memfile() + opts = shasum.gen_opts(algorithm="SHA256", + dest=destfile, + files=[os.path.join(DATADIR, "empty")], + mmap=False) + shasum.shasum(opts) + self.assertTrue( + destfile.getvalue().startswith( + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 *")) + + +if __name__ == "__main__": + sys.exit(unittest.main()) diff -r 4bb2d0975cfe -r a828f320ac58 tests/test_walk.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_walk.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- +r"""Unit tests + +""" + +from __future__ import absolute_import, print_function + +import sys +import unittest + +import _test_setup # noqa: F401 imported but unused + +from cutils.util import walk + + +class SurrogateEscapeTests(unittest.TestCase): + + @unittest.skipIf(sys.version_info[0] >= 3, "Skip on Python3") + def test_simple_escape(self): + w = b"\xc4" + + d = walk.WalkDirEntry.surrogate_decode(w) + self.assertEqual(u"\udcc4", d) + + @unittest.skipIf(sys.version_info[0] >= 3, "Skip on Python3") + def test_no_escape_min(self): + w = b"\x00" + + d = walk.WalkDirEntry.surrogate_decode(w) + self.assertEqual(u"\x00", d) + + @unittest.skipIf(sys.version_info[0] >= 3, "Skip on Python3") + def test_no_escape_max(self): + w = b"\x7f" + + d = walk.WalkDirEntry.surrogate_decode(w) + self.assertEqual(u"\x7f", d) + + @unittest.skipIf(sys.version_info[0] >= 3, "Skip on Python3") + def test_escape_min(self): + w = b"\x80" + + d = walk.WalkDirEntry.surrogate_decode(w) + self.assertEqual(u"\udc80", d) + + @unittest.skipIf(sys.version_info[0] >= 3, "Skip on Python3") + def test_escape_max(self): + w = b"\xff" + + d = walk.WalkDirEntry.surrogate_decode(w) + self.assertEqual(u"\udcff", d) + + @unittest.skipIf(sys.version_info[0] >= 3, "Skip on Python3") + def test_complex(self): + w = b"abc\xc4d\x80\x81\xffefg" + d = walk.WalkDirEntry.surrogate_decode(w) + self.assertEqual(u"abc\udcc4d\udc80\udc81\udcffefg", d) + + +class WalkDirEntryTests(unittest.TestCase): + + def setUp(self): + self._orig_fsencoding = walk._FSENCODING + walk._FSENCODING = "ascii" + + def tearDown(self): + walk._FSENCODING = self._orig_fsencoding + + def test_ascii(self): + entry = walk.WalkDirEntry.from_path_name("tests", "_test_setup.py") + self.assertEqual("_test_setup.py", entry.name) + self.assertEqual("tests/_test_setup.py", entry.path) + self.assertEqual(u"_test_setup.py", entry.uname) + self.assertEqual(u"tests/_test_setup.py", entry.upath) + self.assertEqual(b"_test_setup.py", entry.u8name) + self.assertEqual(b"tests/_test_setup.py", entry.u8path) + self.assertEqual(b"_test_setup.py", entry.alt_u8name) + self.assertEqual(b"tests/_test_setup.py", entry.alt_u8path) + self.assertEqual(b"_test_setup.py", entry.alt_fsname) + self.assertEqual(b"tests/_test_setup.py", entry.alt_fspath) + + @unittest.skipIf(sys.version_info[0] < 3, "Skip on Python2") + def test_with_surrogate_escaped_name(self): + # instantiate with a surrogate escaped path from PEP 383 + entry = walk.WalkDirEntry.from_path_name( + "tests", "test-\udcc4", _do_stat=False) + self.assertEqual("test-\udcc4", entry.name) + self.assertEqual("tests/test-\udcc4", entry.path) + self.assertEqual(b"test-\xc4", entry.fsname) + self.assertEqual(b"tests/test-\xc4", entry.fspath) + self.assertEqual(b"test-\xc4", entry.alt_fsname) + self.assertEqual(b"tests/test-\xc4", entry.alt_fspath) + + self.assertIsNone(entry.uname) + self.assertIsNone(entry.upath) + self.assertIsNone(entry.u8name) + self.assertIsNone(entry.u8path) + + self.assertEqual(b"test-\\udcc4", entry.alt_u8name) + self.assertEqual(b"tests/test-\\udcc4", entry.alt_u8path) + + @unittest.skipIf(sys.version_info[0] < 3, "Skip on Python2") + def test_with_surrogate_escaped_path(self): + # instantiate with a surrogate escaped path from PEP 383 + entry = walk.WalkDirEntry.from_path_name( + "tests\udcc5", "test", _do_stat=False) + self.assertEqual("test", entry.name) + self.assertEqual("tests\udcc5/test", entry.path) + self.assertEqual(b"test", entry.fsname) + self.assertEqual(b"tests\xc5/test", entry.fspath) + self.assertEqual(b"test", entry.alt_fsname) + self.assertEqual(b"tests\xc5/test", entry.alt_fspath) + + self.assertEqual("test", entry.uname) + self.assertIsNone(entry.upath) + self.assertEqual(b"test", entry.u8name) + self.assertIsNone(entry.u8path) + + self.assertEqual(b"test", entry.alt_u8name) + self.assertEqual(b"tests\\udcc5/test", entry.alt_u8path) + + @unittest.skipIf(sys.version_info[0] > 2, "Skip on Python3") + def test_py2_with_non_fsdecodable_name(self): + entry = walk.WalkDirEntry.from_path_name( + b"tests", b"test-\xc4", _do_stat=False) + self.assertEqual(b"test-\xc4", entry.name) + self.assertEqual(b"tests/test-\xc4", entry.path) + self.assertEqual(b"test-\xc4", entry.fsname) + self.assertEqual(b"tests/test-\xc4", entry.fspath) + self.assertEqual(b"test-\xc4", entry.alt_fsname) + self.assertEqual(b"tests/test-\xc4", entry.alt_fspath) + + self.assertIsNone(entry.uname) + self.assertIsNone(entry.upath) + self.assertIsNone(entry.u8name) + self.assertIsNone(entry.u8path) + + self.assertEqual(b"test-\\udcc4", entry.alt_u8name) + self.assertEqual(b"tests/test-\\udcc4", entry.alt_u8path) + + @unittest.skipIf(sys.version_info[0] > 2, "Skip on Python3") + def test_py2_with_non_fsdecodable_path(self): + entry = walk.WalkDirEntry.from_path_name( + b"tests\xc5", b"test", _do_stat=False) + self.assertEqual(b"test", entry.name) + self.assertEqual(b"tests\xc5/test", entry.path) + self.assertEqual(b"test", entry.fsname) + self.assertEqual(b"tests\xc5/test", entry.fspath) + self.assertEqual(b"test", entry.alt_fsname) + self.assertEqual(b"tests\xc5/test", entry.alt_fspath) + + self.assertEqual(b"test", entry.uname) + self.assertIsNone(entry.upath) + self.assertEqual(b"test", entry.u8name) + self.assertIsNone(entry.u8path) + + self.assertEqual(b"test", entry.alt_u8name) + self.assertEqual(b"tests\\udcc5/test", entry.alt_u8path) + + @unittest.skipIf(sys.version_info[0] > 2, "Skip on Python3") + def test_py2_with_non_fsencodable_unicode_name(self): + entry = walk.WalkDirEntry.from_path_name( + u"tests", u"test-\xc4", _do_stat=False) + self.assertEqual(u"test-\xc4", entry.name) + self.assertEqual(u"tests/test-\xc4", entry.path) + self.assertIsNone(entry.fsname) + self.assertIsNone(entry.fspath) + self.assertEqual(b"test-\\xc4", entry.alt_fsname) + self.assertEqual(b"tests/test-\\xc4", entry.alt_fspath) + + self.assertEqual(u"test-\xc4", entry.uname) + self.assertEqual(u"tests/test-\xc4", entry.upath) + self.assertEqual(b"test-\xc3\x84", entry.u8name) + self.assertEqual(b"tests/test-\xc3\x84", entry.u8path) + + self.assertEqual(b"test-\\xc4", entry.alt_u8name) + self.assertEqual(b"tests/test-\\xc4", entry.alt_u8path) + + @unittest.skipIf(sys.version_info[0] > 2, "Skip on Python3") + def test_py2_with_non_fsencodable_unicode_path(self): + entry = walk.WalkDirEntry.from_path_name( + u"tests\xc5", u"test", _do_stat=False) + self.assertEqual(u"test", entry.name) + self.assertEqual(u"tests\xc5/test", entry.path) + self.assertEqual(b"test", entry.fsname) + self.assertIsNone(entry.fspath) + self.assertEqual(b"test", entry.alt_fsname) + self.assertEqual(b"tests\\xc5/test", entry.alt_fspath) + + self.assertEqual(u"test", entry.uname) + self.assertEqual(u"tests\xc5/test", entry.upath) + self.assertEqual(b"test", entry.u8name) + self.assertEqual(b"tests\xc3\x85/test", entry.u8path) + + self.assertEqual(b"test", entry.alt_u8name) + self.assertEqual(b"tests\\xc5/test", entry.alt_u8path) + + +if __name__ == "__main__": + sys.exit(unittest.main()) diff -r 4bb2d0975cfe -r a828f320ac58 treesum.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/treesum.py Fri Feb 07 12:38:14 2025 +0100 @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# :- +# :Copyright: (c) 2020-2025 Franz Glasner +# :License: BSD-3-Clause +# :- +r"""Pure Python implementation of a directory tree checksum. + +""" + +from __future__ import absolute_import + +import sys + +import cutils.treesum + + +sys.exit(cutils.treesum.main())