• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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