comparison shasum.py @ 22:6bdfc5ad4656

Implemented OpenBSD's -C (aka --checklist) option for shasum
author Franz Glasner <fzglas.hg@dom66.de>
date Sat, 05 Dec 2020 15:08:46 +0100
parents f2d634270e1c
children 232063b73e45
comparison
equal deleted inserted replaced
21:f2d634270e1c 22:6bdfc5ad4656
49 be verified against the specified paths. Output consists of the digest 49 be verified against the specified paths. Output consists of the digest
50 used, the file name, and an OK, FAILED, or MISSING for the result of 50 used, the file name, and an OK, FAILED, or MISSING for the result of
51 the comparison. This will validate any of the supported checksums. 51 the comparison. This will validate any of the supported checksums.
52 If no file is given, stdin is used.""") 52 If no file is given, stdin is used.""")
53 aparser.add_argument( 53 aparser.add_argument(
54 "--checklist", "-C", metavar="CHECKLIST",
55 help="""Compare the checksum of each FILE against the checksums in
56 the CHECKLIST. Any specified FILE that is not listed in the CHECKLIST will
57 generate an error.""")
58
59 aparser.add_argument(
54 "--reverse", "-r", action="store_false", dest="bsd", default=False, 60 "--reverse", "-r", action="store_false", dest="bsd", default=False,
55 help="explicitely select normal coreutils style output (to be option compatible with BSD style commands and :command:`openssl dgst -r`)") 61 help="explicitely select normal coreutils style output (to be option compatible with BSD style commands and :command:`openssl dgst -r`)")
56 aparser.add_argument( 62 aparser.add_argument(
57 "--tag", action="store_true", dest="bsd", default=False, 63 "--tag", action="store_true", dest="bsd", default=False,
58 help="alias for the `--bsd' option (to be compatible with :command:`b2sum`)") 64 help="alias for the `--bsd' option (to be compatible with :command:`b2sum`)")
66 72
67 opts = aparser.parse_args(args=argv) 73 opts = aparser.parse_args(args=argv)
68 74
69 if opts.text_mode: 75 if opts.text_mode:
70 print("ERROR: text mode not supported", file=sys.stderr) 76 print("ERROR: text mode not supported", file=sys.stderr)
71 sys.exit(78) # :manpage:`sysexits(3)` EX_CONFIG 77 sys.exit(78) # :manpage:`sysexits(3)` EX_CONFIG
78
79 if opts.check and opts.checklist:
80 print("ERROR: only one of --check or --checklist allowed",
81 file=sys.stderr)
82 sys.exit(64) # :manpage:`sysexits(3)` EX_USAGE
72 83
73 if not opts.algorithm: 84 if not opts.algorithm:
74 opts.algorithm = argv2algo("1") 85 opts.algorithm = argv2algo("1")
75 86
76 if opts.check: 87 if opts.check:
77 return verify_digests_from_files(opts) 88 return verify_digests_from_files(opts)
89 elif opts.checklist:
90 return verify_digests_with_checklist(opts)
78 else: 91 else:
79 return generate_digests(opts) 92 return generate_digests(opts)
80 93
81 94
82 def generate_digests(opts): 95 def generate_digests(opts):
104 compute_digest(opts.algorithm[0], source), 117 compute_digest(opts.algorithm[0], source),
105 fn, 118 fn,
106 opts.algorithm[1], 119 opts.algorithm[1],
107 True) 120 True)
108 return 0 121 return 0
122
123
124 def verify_digests_with_checklist(opts):
125 exit_code = 0
126 if not opts.files or (len(opts.files) == 1 and opts.files[0] == '-'):
127 if PY2:
128 if sys.platform == "win32":
129 import os, msvcrt # noqa: E401
130 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
131 source = sys.stdin
132 else:
133 source = sys.stdin.buffer
134 pl = get_parsed_digest_line_from_checklist(opts.checklist, opts, None)
135 if pl is None:
136 exit_code = 1
137 print("-: MISSING")
138 else:
139 tag, algo, cl_filename, cl_digest = pl
140 computed_digest = compute_digest(algo, source)
141 if cl_digest.lower() == computed_digest.lower():
142 res = "OK"
143 else:
144 res = "FAILED"
145 exit_code = 1
146 print("{}: {}: {}".format(tag, "-", res))
147 else:
148 for fn in opts.files:
149 pl = get_parsed_digest_line_from_checklist(opts.checklist, opts, fn)
150 if pl is None:
151 print("{}: MISSING".format(fn))
152 exit_code = 1
153 else:
154 tag, algo, cl_filename, cl_digest = pl
155 with open(fn, "rb") as source:
156 computed_digest = compute_digest(algo, source)
157 if cl_digest.lower() == computed_digest.lower():
158 res = "OK"
159 else:
160 exit_code = 1
161 res = "FAILED"
162 print("{}: {}: {}".format(tag, fn, res))
163 return exit_code
109 164
110 165
111 def verify_digests_from_files(opts): 166 def verify_digests_from_files(opts):
112 exit_code = 0 167 exit_code = 0
113 if not opts.files or (len(opts.files) == 1 and opts.files[0] == '-'): 168 if not opts.files or (len(opts.files) == 1 and opts.files[0] == '-'):
150 return ("ok", fn, tag) 205 return ("ok", fn, tag)
151 else: 206 else:
152 return ("failed", fn, tag) 207 return ("failed", fn, tag)
153 except EnvironmentError: 208 except EnvironmentError:
154 return ("missing", fn, tag) 209 return ("missing", fn, tag)
210
211
212 def get_parsed_digest_line_from_checklist(checklist, opts, filename):
213 if filename is None:
214 filenames = ("-", "stdin", "", )
215 else:
216 filenames = (
217 normalize_filename(filename, strip_leading_dot_slash=True),)
218 with io.open(checklist, "rt", encoding="utf-8") as clf:
219 for checkline in clf:
220 if not checkline:
221 continue
222 parts = parse_digest_line(opts, checkline)
223 if not parts:
224 raise ValueError(
225 "improperly formatted digest line: {}".format(checkline))
226 fn = normalize_filename(parts[2], strip_leading_dot_slash=True)
227 if fn in filenames:
228 return parts
229 else:
230 return None
155 231
156 232
157 def parse_digest_line(opts, line): 233 def parse_digest_line(opts, line):
158 """Parse a `line` of a digest file and return its parts. 234 """Parse a `line` of a digest file and return its parts.
159 235