checksum.py 7.1 KB

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