Skip to content

Commit aa53cb9

Browse files
committed
Properly handle BOM when decoding UTF-16-LE and clean up arg parsing. Fixes #2.
1 parent 0894475 commit aa53cb9

File tree

2 files changed

+76
-38
lines changed

2 files changed

+76
-38
lines changed

README.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,26 @@ This is a transparent implementation of the Exact Audio Copy log checksum algori
44

55
# Usage
66

7-
usage: eac.py [-h] (--verify | --sign) FILE
7+
usage: eac.py [-h] {verify,sign} ...
88

99
Verifies and resigns EAC logs
1010

1111
positional arguments:
12-
FILE path to the log file
12+
{verify,sign}
13+
verify verify a log
14+
sign sign or fix an existing log
1315

1416
optional arguments:
15-
-h, --help show this help message and exit
16-
--verify verify a log
17-
--sign sign or fix an existing log
17+
-h, --help show this help message and exit
18+
19+
# Example
20+
21+
$ python3 eac.py sign bad.log good.log
22+
$ python3 eac.py verify *.log
23+
log1.log: OK
24+
log2.log: OK
25+
log3.log: Malformed
26+
1827

1928
# Overview
2029

eac.py

Lines changed: 62 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import sys
44
import argparse
5+
import contextlib
56

67
CHECKSUM_MIN_VERSION = ('V1.0', 'beta', '1')
78

@@ -113,6 +114,10 @@ def eac_verify(data):
113114
# Log is encoded as Little Endian UTF-16
114115
text = data.decode('utf-16-le')
115116

117+
# Strip off the BOM
118+
if text.startswith('\ufeff'):
119+
text = text[1:]
120+
116121
# Null bytes screw it up
117122
if '\x00' in text:
118123
text = text[:text.index('\x00')]
@@ -126,46 +131,70 @@ def eac_verify(data):
126131
return unsigned_text, version, old_signature, eac_checksum(unsigned_text)
127132

128133

134+
class FixedFileType(argparse.FileType):
135+
def __call__(self, string):
136+
file = super().__call__(string)
137+
138+
# Properly handle stdin/stdout with 'b' mode
139+
if 'b' in self._mode and file in (sys.stdin, sys.stdout):
140+
return file.buffer
141+
142+
return file
143+
144+
129145
if __name__ == '__main__':
130146
parser = argparse.ArgumentParser(description='Verifies and resigns EAC logs')
131-
parser.add_argument('file', metavar='FILE', help='path to the log file')
132147

133-
group = parser.add_mutually_exclusive_group(required=True)
134-
group.add_argument('--verify', action='store_true', help='verify a log')
135-
group.add_argument('--sign', action='store_true', help='sign or fix an existing log')
148+
subparsers = parser.add_subparsers(dest='command', required=True)
149+
150+
verify_parser = subparsers.add_parser('verify', help='verify a log')
151+
verify_parser.add_argument('files', type=FixedFileType(mode='rb'), nargs='+', help='input log file(s)')
152+
153+
sign_parser = subparsers.add_parser('sign', help='sign or fix an existing log')
154+
sign_parser.add_argument('--force', action='store_true', help='forces signing even if EAC version is too old')
155+
sign_parser.add_argument('input_file', type=FixedFileType(mode='rb'), help='input log file')
156+
sign_parser.add_argument('output_file', type=FixedFileType(mode='wb'), help='output log file')
136157

137158
args = parser.parse_args()
138159

139-
if args.file == '-':
140-
handle = sys.stdin
141-
else:
142-
handle = open(args.file, 'rb')
143-
144-
try:
145-
data, version, old_signature, actual_signature = eac_verify(handle.read())
146-
except RuntimeError as e:
147-
print(e)
148-
sys.exit(1)
149-
finally:
150-
handle.close()
151-
152-
if args.sign:
153-
if version <= CHECKSUM_MIN_VERSION:
160+
if args.command == 'sign':
161+
with contextlib.closing(args.input_file) as handle:
162+
try:
163+
data, version, old_signature, actual_signature = eac_verify(handle.read())
164+
except ValueError as e:
165+
print(args.input_file, ': ', e, sep='')
166+
sys.exit(1)
167+
168+
if not args.force and (version is None or version <= CHECKSUM_MIN_VERSION):
154169
raise ValueError('EAC version is too old to be signed')
155170

156171
data += f'\r\n\r\n==== Log checksum {actual_signature} ====\r\n'
157172

158-
sys.stdout.buffer.write(b'\xff\xfe' + data.encode('utf-16le'))
159-
160-
if args.verify:
161-
if old_signature is None:
162-
print('Not a signed log file')
163-
sys.exit(1)
164-
elif old_signature != actual_signature:
165-
print('Malformed')
166-
sys.exit(1)
167-
elif version <= CHECKSUM_MIN_VERSION:
168-
print('Forged')
169-
sys.exit(1)
170-
else:
171-
print('OK')
173+
with contextlib.closing(args.output_file or args.input_file) as handle:
174+
handle.write(b'\xff\xfe' + data.encode('utf-16le'))
175+
elif args.command == 'verify':
176+
max_length = max(len(f.name) for f in args.files)
177+
178+
for file in args.files:
179+
prefix = (file.name + ':').ljust(max_length + 2)
180+
181+
with contextlib.closing(file) as handle:
182+
try:
183+
data, version, old_signature, actual_signature = eac_verify(handle.read())
184+
except RuntimeError as e:
185+
print(prefix, e)
186+
continue
187+
except ValueError as e:
188+
print(prefix, 'Not a log file')
189+
continue
190+
191+
if version is None:
192+
print(prefix, 'Not a log file')
193+
elif old_signature is None:
194+
print(prefix, 'Log file without a signature')
195+
elif old_signature != actual_signature:
196+
print(prefix, 'Malformed')
197+
elif version <= CHECKSUM_MIN_VERSION:
198+
print(prefix, 'Forged')
199+
else:
200+
print(prefix, 'OK')

0 commit comments

Comments
 (0)