Add support for Join sets where the parity protects the split files
[rarslave2.git] / PAR2Set / par2parser.py
1 #!/usr/bin/env python
2 # vim: set ts=4 sts=4 sw=4 textwidth=80:
3
4 # This breaks my convention, but python requires that it be
5 # at the top of the file ...
6 from __future__ import with_statement
7
8 """
9 Module which holds PAR2 file parsing functions.
10
11 Much of this code was borrowed from the excellent cfv project.
12 See http://cfv.sourceforge.net/ for a copy.
13 """
14
15 __author__    = "Ira W. Snyder (devel@irasnyder.com)"
16 __copyright__ = "Copyright (c) 2006-2008 Ira W. Snyder (devel@irasnyder.com)"
17 __license__   = "GNU GPL v2 (or, at your option, any later version)"
18
19 #    par2parser.py -- PAR2 file parsing utility
20 #
21 #    Copyright (C) 2006-2008  Ira W. Snyder (devel@irasnyder.com)
22 #
23 #    This program is free software; you can redistribute it and/or modify
24 #    it under the terms of the GNU General Public License as published by
25 #    the Free Software Foundation; either version 2 of the License, or
26 #    (at your option) any later version.
27 #
28 #    This program is distributed in the hope that it will be useful,
29 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
30 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
31 #    GNU General Public License for more details.
32 #
33 #    You should have received a copy of the GNU General Public License
34 #    along with this program; if not, write to the Free Software
35 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
36
37
38 import struct, errno, os, md5
39
40 ################################################################################
41
42 def chompnulls(line):
43     p = line.find('\0')
44
45     if p < 0:
46         return line
47     else:
48         return line[:p]
49
50 ################################################################################
51
52 # Get all of the filenames protected by the parityFile
53 #
54 # This code was almost copied from the CFV project
55 # See the header of this file for more information
56 def getProtectedFiles(directory, parityFile):
57
58     assert os.path.isdir(directory)
59     assert os.path.isfile(os.path.join(directory, parityFile))
60
61     with open(os.path.join(directory, parityFile), 'rb') as file:
62
63         # We always want to do crc checks
64         docrcchecks = True
65
66         pkt_header_fmt = '< 8s Q 16s 16s 16s'
67         pkt_header_size = struct.calcsize(pkt_header_fmt)
68         file_pkt_fmt = '< 16s 16s 16s Q'
69         file_pkt_size = struct.calcsize(file_pkt_fmt)
70         main_pkt_fmt = '< Q I'
71         main_pkt_size = struct.calcsize(main_pkt_fmt)
72
73         seen_file_ids = {}
74         expected_file_ids = None
75         filenames = []
76
77         while True:
78             d = file.read(pkt_header_size)
79             if not d:
80                 break
81
82             magic, pkt_len, pkt_md5, set_id, pkt_type = struct.unpack(pkt_header_fmt, d)
83
84             if docrcchecks:
85                 control_md5 = md5.new()
86                 control_md5.update(d[0x20:])
87                 d = file.read(pkt_len - pkt_header_size)
88                 control_md5.update(d)
89
90                 if control_md5.digest() != pkt_md5:
91                     raise EnvironmentError, (errno.EINVAL, \
92                         "corrupt par2 file - bad packet hash")
93
94             if pkt_type == 'PAR 2.0\0FileDesc':
95                 if not docrcchecks:
96                     d = file.read(pkt_len - pkt_header_size)
97
98                 file_id, file_md5, file_md5_16k, file_size = \
99                     struct.unpack(file_pkt_fmt, d[:file_pkt_size])
100
101                 if seen_file_ids.get(file_id) is None:
102                     seen_file_ids[file_id] = 1
103                     filename = chompnulls(d[file_pkt_size:])
104                     filenames.append(filename)
105
106             elif pkt_type == "PAR 2.0\0Main\0\0\0\0":
107                 if not docrcchecks:
108                     d = file.read(pkt_len - pkt_header_size)
109
110                 if expected_file_ids is None:
111                     expected_file_ids = []
112                     slice_size, num_files = struct.unpack(main_pkt_fmt, d[:main_pkt_size])
113                     num_nonrecovery = (len(d)-main_pkt_size)/16 - num_files
114
115                     for i in range(main_pkt_size,main_pkt_size+(num_files+num_nonrecovery)*16,16):
116                         expected_file_ids.append(d[i:i+16])
117
118             else:
119                 if not docrcchecks:
120                     file.seek(pkt_len - pkt_header_size, 1)
121
122         if expected_file_ids is None:
123             raise EnvironmentError, (errno.EINVAL, \
124                 "corrupt or unsupported par2 file - no main packet found")
125
126         for id in expected_file_ids:
127             if not seen_file_ids.has_key(id):
128                 raise EnvironmentError, (errno.EINVAL, \
129                     "corrupt or unsupported par2 file - " \
130                     "expected file description packet not found")
131
132     return filenames
133
134 ################################################################################
135
136 def main():
137     pass
138
139 ################################################################################
140
141 if __name__ == '__main__':
142     main()
143