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 # important to keep the newlinestr value defined here as hash values will change across platforms 33 # if platform specific newline values are assumed 34 tt.saveXML(temp_ttx_path, newlinestr="\n", skipTables=exclude_tables, tables=include_tables) 35 checksum_path = temp_ttx_path 36 else: 37 if include_tables is not None: 38 sys.stderr.write("[checksum.py] -i and --include are not supported for font binary filepaths. \ 39 Use these flags for checksums with the --ttx flag.") 40 sys.exit(1) 41 if exclude_tables is not None: 42 sys.stderr.write("[checksum.py] -e and --exclude are not supported for font binary filepaths. \ 43 Use these flags for checksums with the --ttx flag.") 44 sys.exit(1) 45 checksum_path = path 46 47 file_contents = _read_binary(checksum_path) 48 49 # store SHA1 hash data and associated file path basename in the checksum_dict dictionary 50 checksum_dict[basename(checksum_path)] = hashlib.sha1(file_contents).hexdigest() 51 52 # remove temp ttx files when present 53 if use_ttx and do_not_cleanup is False: 54 os.remove(temp_ttx_path) 55 56 # generate the checksum list string for writes 57 checksum_out_data = "" 58 for key in checksum_dict.keys(): 59 checksum_out_data += checksum_dict[key] + " " + key + "\n" 60 61 # write to stdout stream or file based upon user request (default = file write) 62 if stdout_write: 63 sys.stdout.write(checksum_out_data) 64 else: 65 checksum_report_filepath = "checksum.txt" 66 with open(checksum_report_filepath, "w") as file: 67 file.write(checksum_out_data) 68 69 70def check_checksum(filepaths): 71 check_failed = False 72 for path in filepaths: 73 if not os.path.exists(path): 74 sys.stderr.write("[checksum.py] ERROR: " + path + " is not a valid filepath" + os.linesep) 75 sys.exit(1) 76 77 with open(path, mode='r') as file: 78 for line in file.readlines(): 79 cleaned_line = line.rstrip() 80 line_list = cleaned_line.split(" ") 81 # eliminate empty strings parsed from > 1 space characters 82 line_list = list(filter(None, line_list)) 83 if len(line_list) == 2: 84 expected_sha1 = line_list[0] 85 test_path = line_list[1] 86 else: 87 sys.stderr.write("[checksum.py] ERROR: failed to parse checksum file values" + os.linesep) 88 sys.exit(1) 89 90 if not os.path.exists(test_path): 91 print(test_path + ": Filepath is not valid, ignored") 92 else: 93 file_contents = _read_binary(test_path) 94 observed_sha1 = hashlib.sha1(file_contents).hexdigest() 95 if observed_sha1 == expected_sha1: 96 print(test_path + ": OK") 97 else: 98 print("-" * 80) 99 print(test_path + ": === FAIL ===") 100 print("Expected vs. Observed:") 101 print(expected_sha1) 102 print(observed_sha1) 103 print("-" * 80) 104 check_failed = True 105 106 # exit with status code 1 if any fails detected across all tests in the check 107 if check_failed is True: 108 sys.exit(1) 109 110 111def _read_binary(filepath): 112 with open(filepath, mode='rb') as file: 113 return file.read() 114 115 116if __name__ == '__main__': 117 parser = argparse.ArgumentParser(prog="checksum.py", description="A SHA1 hash checksum list generator and checksum testing script") 118 parser.add_argument("-t", "--ttx", help="Calculate from ttx file", action="store_true") 119 parser.add_argument("-s", "--stdout", help="Write output to stdout stream", action="store_true") 120 parser.add_argument("-n", "--noclean", help="Do not discard *.ttx files used to calculate SHA1 hashes", action="store_true") 121 parser.add_argument("-c", "--check", help="Verify checksum values vs. files", action="store_true") 122 parser.add_argument("filepaths", nargs="+", help="One or more file paths. Use checksum file path for -c/--check. Use paths\ 123 to font files for all other commands.") 124 125 parser.add_argument("-i", "--include", action="append", help="Included OpenType tables for ttx data dump") 126 parser.add_argument("-e", "--exclude", action="append", help="Excluded OpenType tables for ttx data dump") 127 128 args = parser.parse_args(sys.argv[1:]) 129 130 if args.check is True: 131 check_checksum(args.filepaths) 132 else: 133 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) 134