| #!/usr/bin/env python3.8 |
| # Copyright 2019 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """ |
| generate hash signature of a directory and optionally compare it to an existing |
| one |
| |
| This script reads all files under DIR to generate SHA-1 hashes from their |
| content. By default, the hash signature is printed and the script exits. |
| |
| The --compare SIGNATURE option can be used to check the hashes against a fixed |
| signature file. SIGNATURE should be the path to an input text file containing |
| one such signature. In this mode, the program's status will be 0 in case of |
| success, or 1 in case of failure. |
| """ |
| |
| import argparse |
| import hashlib |
| import json |
| import os |
| import sys |
| |
| SHA1_HEX_LEN = 40 |
| CHUNK_SIZE = 4096 |
| MESSAGE = """signature has changed: |
| %(changes)s |
| |
| (tip) To update the signature run: |
| $ %(name)s %(dir)s > %(signature)s""" |
| |
| |
| def main(name, argv): |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument('path', metavar='DIR', help='path to directory') |
| parser.add_argument('-c', '--compare', metavar='SIGNATURE', |
| help='path to input signature file') |
| |
| args = parser.parse_args(argv) |
| signatures = dir_signatures(args.path) |
| |
| if not args.compare: |
| print(json.dumps(signatures, sort_keys=True, indent=4)) |
| else: |
| try: |
| with open(args.compare, 'rb') as file: |
| expected = json.loads(file.read()) |
| |
| current_paths = set(signatures.keys()) |
| expected_paths = set(expected.keys()) |
| |
| added = sorted(current_paths - expected_paths) |
| removed = sorted(expected_paths - current_paths) |
| changed = sorted([path for path in |
| current_paths.intersection(expected_paths) |
| if signatures[path] != expected[path]]) |
| |
| if added or removed or changed: |
| changes = '\n '.join([p + ' (added)' for p in added] + |
| [p + ' (changed)' for p in changed] + |
| [p + ' (removed)' for p in removed]) |
| |
| values = { |
| 'changes': changes, |
| 'name': name, |
| 'dir': args.path, |
| 'signature': args.compare |
| } |
| |
| sys.exit(MESSAGE % values) |
| else: |
| print('signatures match') |
| |
| |
| except OSError: |
| sys.exit('could not read signature from: %s' % args.compare) |
| |
| |
| def dir_signatures(path): |
| if not os.path.exists(path): |
| sys.exit('could not find path: %s' % path) |
| |
| signatures = {} |
| file_paths = sorted([os.path.join(root, file) |
| for root, _, files in os.walk(path) |
| for file in files]) |
| |
| for file_path in file_paths: |
| try: |
| with open(file_path, 'rb') as file: |
| hash = hashlib.sha1() |
| while True: |
| chunk = file.read(CHUNK_SIZE) |
| if not chunk: |
| break |
| hash.update(hashlib.sha1(chunk).hexdigest().encode('utf-8')) |
| |
| relative = os.path.relpath(file_path, path) |
| signatures[relative] = hash.hexdigest() |
| except OSError: |
| sys.exit('could not read: %s' % path) |
| |
| return signatures |
| |
| |
| if __name__ == "__main__": |
| main(sys.argv[0], sys.argv[1:]) |