[RARSLAVE] Add PAR2 Parser
[rarslave2.git] / par2parser.py
1 #!/usr/bin/env python
2 # vim: set ts=4 sts=4 sw=4 textwidth=92:
3
4 ################################################################################
5 # The PAR2 Parser
6 #
7 # This was stolen from cfv (see http://cfv.sourceforge.net/ for a copy)
8 ################################################################################
9
10 import struct, errno, os
11
12 def get_full_filename (dir, file):
13         return os.path.abspath (os.path.expanduser (os.path.join (dir, file)))
14
15 def chompnulls(line):
16     p = line.find('\0')
17     if p < 0: return line
18     else:     return line[:p]
19
20 def get_protected_files (dir, filename):
21     """Get all of the filenames that are protected by the par2
22     file given as the filename"""
23
24     full_filename = get_full_filename (dir, filename)
25
26     try:
27         file = open(full_filename, 'rb')
28     except:
29         print 'Could not open %s' % (full_filename, )
30         return []
31
32     # We always want to do crc checks
33     docrcchecks = True
34
35     pkt_header_fmt = '< 8s Q 16s 16s 16s'
36     pkt_header_size = struct.calcsize(pkt_header_fmt)
37     file_pkt_fmt = '< 16s 16s 16s Q'
38     file_pkt_size = struct.calcsize(file_pkt_fmt)
39     main_pkt_fmt = '< Q I'
40     main_pkt_size = struct.calcsize(main_pkt_fmt)
41
42     seen_file_ids = {}
43     expected_file_ids = None
44     filenames = []
45
46     while 1:
47         d = file.read(pkt_header_size)
48         if not d:
49             break
50
51         magic, pkt_len, pkt_md5, set_id, pkt_type = struct.unpack(pkt_header_fmt, d)
52
53         if docrcchecks:
54             import md5
55             control_md5 = md5.new()
56             control_md5.update(d[0x20:])
57             d = file.read(pkt_len - pkt_header_size)
58             control_md5.update(d)
59
60             if control_md5.digest() != pkt_md5:
61                 raise EnvironmentError, (errno.EINVAL, \
62                     "corrupt par2 file - bad packet hash")
63
64         if pkt_type == 'PAR 2.0\0FileDesc':
65             if not docrcchecks:
66                 d = file.read(pkt_len - pkt_header_size)
67
68             file_id, file_md5, file_md5_16k, file_size = \
69                 struct.unpack(file_pkt_fmt, d[:file_pkt_size])
70
71             if seen_file_ids.get(file_id) is None:
72                 seen_file_ids[file_id] = 1
73                 filename = chompnulls(d[file_pkt_size:])
74                 filenames.append(filename)
75
76         elif pkt_type == "PAR 2.0\0Main\0\0\0\0":
77             if not docrcchecks:
78                 d = file.read(pkt_len - pkt_header_size)
79
80             if expected_file_ids is None:
81                 expected_file_ids = []
82                 slice_size, num_files = struct.unpack(main_pkt_fmt, d[:main_pkt_size])
83                 num_nonrecovery = (len(d)-main_pkt_size)/16 - num_files
84
85                 for i in range(main_pkt_size,main_pkt_size+(num_files+num_nonrecovery)*16,16):
86                     expected_file_ids.append(d[i:i+16])
87
88         else:
89             if not docrcchecks:
90                 file.seek(pkt_len - pkt_header_size, 1)
91
92     if expected_file_ids is None:
93         raise EnvironmentError, (errno.EINVAL, \
94             "corrupt or unsupported par2 file - no main packet found")
95
96     for id in expected_file_ids:
97         if not seen_file_ids.has_key(id):
98             raise EnvironmentError, (errno.EINVAL, \
99                 "corrupt or unsupported par2 file - " \
100                 "expected file description packet not found")
101
102     return filenames
103
104 def main ():
105         pass
106
107 if __name__ == '__main__':
108         main ()
109