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