11#!/usr/bin/env python3
22# -*- coding: utf-8 -*-
33
4- #----------------------------------------------------------------
4+ #------------------------------------------------------------------------------------------------------
55# checksum.py
66# A SHA1 hash checksum list generator for fonts and fontTools
7- # XML dumps of font OpenType table data
7+ # XML dumps of font OpenType table data + checksum testing
8+ # script
89#
910# Copyright 2018 Christopher Simpkins
1011# MIT License
1112#
13+ # Dependencies: Python fontTools library
14+ #
1215# Usage: checksum.py (options) [file path 1]...[file path n]
1316#
14- # Dependencies: Python fontTools library
15- #----------------------------------------------------------------
17+ # Options:
18+ # -h, --help Help
19+ # -t, --ttx Calculate SHA1 hash values from ttx dump of XML (default = font binary)
20+ # -s, --stdout Stream to standard output stream (default = write to disk as 'checksum.txt')
21+ # -c, --check Test SHA1 checksum values against a valid checksum file
22+ #
23+ # Options, --ttx only:
24+ # -e, --exclude Excluded OpenType table (may be used more than once, mutually exclusive with -i)
25+ # -i, --include Included OpenType table (may be used more than once, mutually exclusive with -e)
26+ # -n, --noclean Do not discard .ttx files that are used to calculate SHA1 hashes
27+ #-------------------------------------------------------------------------------------------------------
1628
1729import argparse
1830import hashlib
2436from fontTools .ttLib import TTFont
2537
2638
27- def main (filepaths , stdout_write = False , use_ttx = False , include_tables = None , exclude_tables = None , do_not_cleanup = False ):
39+ def write_checksum (filepaths , stdout_write = False , use_ttx = False , include_tables = None , exclude_tables = None , do_not_cleanup = False ):
2840 checksum_dict = {}
2941 for path in filepaths :
3042 if not os .path .exists (path ):
@@ -42,6 +54,8 @@ def main(filepaths, stdout_write=False, use_ttx=False, include_tables=None, excl
4254 temp_ttx_path = path + ".ttx"
4355
4456 tt = TTFont (path )
57+ # important to keep the newlinestr value defined here as hash values will change across platforms
58+ # if platform specific newline values are assumed
4559 tt .saveXML (temp_ttx_path , newlinestr = "\n " , skipTables = exclude_tables , tables = include_tables )
4660 checksum_path = temp_ttx_path
4761 else :
@@ -55,7 +69,7 @@ def main(filepaths, stdout_write=False, use_ttx=False, include_tables=None, excl
5569 sys .exit (1 )
5670 checksum_path = path
5771
58- file_contents = read_binary (checksum_path )
72+ file_contents = _read_binary (checksum_path )
5973
6074 # store SHA1 hash data and associated file path basename in the checksum_dict dictionary
6175 checksum_dict [basename (checksum_path )] = hashlib .sha1 (file_contents ).hexdigest ()
@@ -78,7 +92,48 @@ def main(filepaths, stdout_write=False, use_ttx=False, include_tables=None, excl
7892 file .write (checksum_out_data )
7993
8094
81- def read_binary (filepath ):
95+ def check_checksum (filepaths ):
96+ check_failed = False
97+ for path in filepaths :
98+ if not os .path .exists (path ):
99+ sys .stderr .write ("[checksum.py] ERROR: " + filepath + " is not a valid filepath" + os .linesep )
100+ sys .exit (1 )
101+
102+ with open (path , mode = 'r' ) as file :
103+ for line in file .readlines ():
104+ cleaned_line = line .rstrip ()
105+ line_list = cleaned_line .split (" " )
106+ # eliminate empty strings parsed from > 1 space characters
107+ line_list = list (filter (None , line_list ))
108+ if len (line_list ) == 2 :
109+ expected_sha1 = line_list [0 ]
110+ test_path = line_list [1 ]
111+ else :
112+ sys .stderr .write ("[checksum.py] ERROR: failed to parse checksum file values" + os .linesep )
113+ sys .exit (1 )
114+
115+ if not os .path .exists (test_path ):
116+ print (test_path + ": Filepath is not valid, ignored" )
117+ else :
118+ file_contents = _read_binary (test_path )
119+ observed_sha1 = hashlib .sha1 (file_contents ).hexdigest ()
120+ if observed_sha1 == expected_sha1 :
121+ print (test_path + ": OK" )
122+ else :
123+ print ("-" * 80 )
124+ print (test_path + ": === FAIL ===" )
125+ print ("Expected vs. Observed:" )
126+ print (expected_sha1 )
127+ print (observed_sha1 )
128+ print ("-" * 80 )
129+ check_failed = True
130+
131+ # exit with status code 1 if any fails detected across all tests in the check
132+ if check_failed is True :
133+ sys .exit (1 )
134+
135+
136+ def _read_binary (filepath ):
82137 with open (filepath , mode = 'rb' ) as file :
83138 return file .read ()
84139
@@ -88,11 +143,15 @@ def read_binary(filepath):
88143 parser .add_argument ("-t" , "--ttx" , help = "Calculate from ttx file" , action = "store_true" )
89144 parser .add_argument ("-s" , "--stdout" , help = "Write output to stdout stream" , action = "store_true" )
90145 parser .add_argument ("-n" , "--noclean" , help = "Do not discard *.ttx files used to calculate SHA1 hashes" , action = "store_true" )
146+ parser .add_argument ("-c" , "--check" , help = "Verify checksum values vs. files" , action = "store_true" )
91147 parser .add_argument ("filepaths" , nargs = "+" , help = "One or more file paths to font binary files" )
92148
93149 parser .add_argument ("-i" , "--include" , action = "append" , help = "Included OpenType tables for ttx data dump" )
94150 parser .add_argument ("-e" , "--exclude" , action = "append" , help = "Excluded OpenType tables for ttx data dump" )
95151
96152 args = parser .parse_args (sys .argv [1 :])
97153
98- main (args .filepaths , stdout_write = args .stdout , use_ttx = args .ttx , do_not_cleanup = args .noclean , include_tables = args .include , exclude_tables = args .exclude )
154+ if args .check is True :
155+ check_checksum (args .filepaths )
156+ else :
157+ 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 )
0 commit comments