1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3 4import argparse 5import hashlib 6import os 7import sys 8 9from os.path import basename 10 11from fontTools.ttLib import TTFont 12 13 14def write_checksum(filepaths, stdout_write=False, use_ttx=False, include_tables=None, exclude_tables=None, do_not_cleanup=False): 15 checksum_dict = {} 16 for path in filepaths: 17 if not os.path.exists(path): 18 sys.stderr.write("[checksum.py] ERROR: " + path + " is not a valid file path" + os.linesep) 19 sys.exit(1) 20 21 if use_ttx: 22 # append a .ttx extension to existing extension to maintain data about the binary that 23 # was used to generate the .ttx XML dump. This creates unique checksum path values for 24 # paths that would otherwise not be unique with a file extension replacement with .ttx 25 # An example is woff and woff2 web font files that share the same base file name: 26 # 27 # coolfont-regular.woff ==> coolfont-regular.ttx 28 # coolfont-regular.woff2 ==> coolfont-regular.ttx (KAPOW! checksum data lost as this would overwrite dict value) 29 temp_ttx_path = path + ".ttx" 30 31 tt = TTFont(path) 32 tt.saveXML(temp_ttx_path, skipTables=exclude_tables, tables=include_tables) 33 checksum_path = temp_ttx_path 34 else: 35 if include_tables is not None: 36 sys.stderr.write("[checksum.py] -i and --include are not supported for font binary filepaths. \ 37 Use these flags for checksums with the --ttx flag.") 38 sys.exit(1) 39 if exclude_tables is not None: 40 sys.stderr.write("[checksum.py] -e and --exclude are not supported for font binary filepaths. \ 41 Use these flags for checksums with the --ttx flag.") 42 sys.exit(1) 43 checksum_path = path 44 45 file_contents = _read_binary(checksum_path) 46 47 # store SHA1 hash data and associated file path basename in the checksum_dict dictionary 48 checksum_dict[basename(checksum_path)] = hashlib.sha1(file_contents).hexdigest() 49 50 # remove temp ttx files when present 51 if use_ttx and do_not_cleanup is False: 52 os.remove(temp_ttx_path) 53 54 # generate the checksum list string for writes 55 checksum_out_data = "" 56 for key in checksum_dict.keys(): 57 checksum_out_data += checksum_dict[key] + " " + key + "\n" 58 59 # write to stdout stream or file based upon user request (default = file write) 60 if stdout_write: 61 sys.stdout.write(checksum_out_data) 62 else: 63 checksum_report_filepath = "checksum.txt" 64 with open(checksum_report_filepath, "w") as file: 65 file.write(checksum_out_data) 66 67 68def check_checksum(filepaths): 69 check_failed = False 70 for path in filepaths: 71 if not os.path.exists(path): 72 sys.stderr.write("[checksum.py] ERROR: " + path + " is not a valid filepath" + os.linesep) 73 sys.exit(1) 74 75 with open(path, mode='r') as file: 76 for line in file.readlines(): 77 cleaned_line = line.rstrip() 78 line_list = cleaned_line.split(" ") 79 # eliminate empty strings parsed from > 1 space characters 80 line_list = list(filter(None, line_list)) 81 if len(line_list) == 2: 82 expected_sha1 = line_list[0] 83 test_path = line_list[1] 84 else: 85 sys.stderr.write("[checksum.py] ERROR: failed to parse checksum file values" + os.linesep) 86 sys.exit(1) 87 88 if not os.path.exists(test_path): 89 print(test_path + ": Filepath is not valid, ignored") 90 else: 91 file_contents = _read_binary(test_path) 92 observed_sha1 = hashlib.sha1(file_contents).hexdigest() 93 if observed_sha1 == expected_sha1: 94 print(test_path + ": OK") 95 else: 96 print("-" * 80) 97 print(test_path + ": === FAIL ===") 98 print("Expected vs. Observed:") 99 print(expected_sha1) 100 print(observed_sha1) 101 print("-" * 80) 102 check_failed = True 103 104 # exit with status code 1 if any fails detected across all tests in the check 105 if check_failed is True: 106 sys.exit(1) 107 108 109def _read_binary(filepath): 110 with open(filepath, mode='rb') as file: 111 return file.read() 112 113 114if __name__ == '__main__': 115 parser = argparse.ArgumentParser(prog="checksum.py", description="A SHA1 hash checksum list generator and checksum testing script") 116 parser.add_argument("-t", "--ttx", help="Calculate from ttx file", action="store_true") 117 parser.add_argument("-s", "--stdout", help="Write output to stdout stream", action="store_true") 118 parser.add_argument("-n", "--noclean", help="Do not discard *.ttx files used to calculate SHA1 hashes", action="store_true") 119 parser.add_argument("-c", "--check", help="Verify checksum values vs. files", action="store_true") 120 parser.add_argument("filepaths", nargs="+", help="One or more file paths. Use checksum file path for -c/--check. Use paths\ 121 to font files for all other commands.") 122 123 parser.add_argument("-i", "--include", action="append", help="Included OpenType tables for ttx data dump") 124 parser.add_argument("-e", "--exclude", action="append", help="Excluded OpenType tables for ttx data dump") 125 126 args = parser.parse_args(sys.argv[1:]) 127 128 if args.check is True: 129 check_checksum(args.filepaths) 130 else: 131 write_checksum(args.filepaths, stdout_write=args.stdout, use_ttx=args.ttx, do_not_cleanup=args.noclean, include_tables=args.include, exclude_tables=args.exclude) 132