diff --git a/pytools/__init__.py b/pytools/__init__.py index ec878b57659ff92db73ad5c67361ed8817097007..e7d2306a7b04863dc4405ad37a5c4b11f01a5c82 100644 --- a/pytools/__init__.py +++ b/pytools/__init__.py @@ -169,6 +169,11 @@ Backports of newer Python functionality .. autofunction:: resolve_name +Hashing +------- + +.. autofunction:: unordered_hash + Type Variables Used ------------------- @@ -2605,6 +2610,37 @@ def resolve_name(name): # }}} +# {{{ unordered_hash + +def unordered_hash(hash_constructor, iterable): + """Using a hash algorithm given by the parameter-less constructor + *hash_constructor*, return a hash object whose internal state + depends on the entries of *iterable*, but not their order. If *hash* + is the instance returned by evaluating ``hash_constructor()``, then + the each entry *i* of the iterable must permit ``hash.upate(i)`` to + succeed. An example of *hash_constructor* is :class:`hashlib.sha256`. + ``hash.digest_size`` must also be defined. + + .. warning:: + + The construction used in this function is likely not cryptographically + secure. Do not use this function in a security-relevant context. + + .. versionadded:: 2021.2 + """ + h_int = 0 + for i in iterable: + h_i = hash_constructor() + h_i.update(i) + h_int = h_int ^ int.from_bytes(h_i.digest(), sys.byteorder) + + h = hash_constructor() + h.update(h_int.to_bytes(h.digest_size, sys.byteorder)) + return h + +# }}} + + def _test(): import doctest doctest.testmod() diff --git a/test/test_pytools.py b/test/test_pytools.py index 87b5d3a76d2d76e4a61e8343252e9b81cd7c767c..59cfc92b0fa4f9b1c57117d8ac734f1b706fa6da 100644 --- a/test/test_pytools.py +++ b/test/test_pytools.py @@ -370,6 +370,26 @@ def test_tag(): t4.without_tags(red_ribbon) +def test_unordered_hash(): + import random + import hashlib + + lst = [random.randbytes(20) for _ in range(200)] + lorig = lst[:] + random.shuffle(lst) + + from pytools import unordered_hash + assert (unordered_hash(hashlib.sha256, lorig).digest() + == unordered_hash(hashlib.sha256, lst).digest()) + assert (unordered_hash(hashlib.sha256, lorig).digest() + == unordered_hash(hashlib.sha256, lorig).digest()) + assert (unordered_hash(hashlib.sha256, lorig).digest() + != unordered_hash(hashlib.sha256, lorig[:-1]).digest()) + lst[0] = b"aksdjfla;sdfjafd" + assert (unordered_hash(hashlib.sha256, lorig).digest() + != unordered_hash(hashlib.sha256, lst).digest()) + + if __name__ == "__main__": if len(sys.argv) > 1: exec(sys.argv[1])