1 # Copyright (C) 2012, Ansgar Burchardt <ansgar@debian.org>
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 """module to handle uploads not yet installed to the archive
19 This module provides classes to handle uploads not yet installed to the
20 archive. Central is the `Changes` class which represents a changes file.
21 It provides methods to access the included binary and source packages.
28 from .gpg import SignedFile
29 from .regexes import *
31 class InvalidChangesException(Exception):
34 class InvalidBinaryException(Exception):
37 class InvalidSourceException(Exception):
40 class InvalidHashException(Exception):
41 def __init__(self, filename, hash_name, expected, actual):
42 self.filename = filename
43 self.hash_name = hash_name
44 self.expected = expected
47 return "Invalid {0} hash for {1}: expected {2}, but got {3}.".format(self.hash_name, self.filename, self.expected, self.actual)
49 class InvalidFilenameException(Exception):
50 def __init__(self, filename):
51 self.filename = filename
53 return "Invalid filename '{0}'.".format(self.filename)
55 class HashedFile(object):
56 """file with checksums
59 filename (str): name of the file
60 size (long): size in bytes
61 md5sum (str): MD5 hash in hexdigits
62 sha1sum (str): SHA1 hash in hexdigits
63 sha256sum (str): SHA256 hash in hexdigits
64 section (str): section or None
65 priority (str): priority or None
67 def __init__(self, filename, size, md5sum, sha1sum, sha256sum, section=None, priority=None):
68 self.filename = filename
71 self.sha1sum = sha1sum
72 self.sha256sum = sha256sum
73 self.section = section
74 self.priority = priority
76 def check(self, directory):
79 Check if size and hashes match the expected value.
82 directory (str): directory the file is located in
85 InvalidHashException: hash mismatch
87 path = os.path.join(directory, self.filename)
90 size = os.stat(path).st_size
92 raise InvalidHashException(self.filename, 'size', self.size, size)
94 md5sum = apt_pkg.md5sum(fh)
95 if md5sum != self.md5sum:
96 raise InvalidHashException(self.filename, 'md5sum', self.md5sum, md5sum)
99 sha1sum = apt_pkg.sha1sum(fh)
100 if sha1sum != self.sha1sum:
101 raise InvalidHashException(self.filename, 'sha1sum', self.sha1sum, sha1sum)
104 sha256sum = apt_pkg.sha256sum(fh)
105 if sha256sum != self.sha256sum:
106 raise InvalidHashException(self.filename, 'sha256sum', self.sha256sum, sha256sum)
108 def parse_file_list(control, has_priority_and_section):
109 """Parse Files and Checksums-* fields
112 control (dict-like): control file to take fields from
113 has_priority_and_section (bool): Files include section and priority (as in .changes)
116 InvalidChangesException: missing fields or other grave errors
119 dictonary mapping filenames to `daklib.upload.HashedFile` objects
123 for line in control["Files"].split('\n'):
127 if has_priority_and_section:
128 (md5sum, size, section, priority, filename) = line.split()
129 entry = dict(md5sum=md5sum, size=long(size), section=section, priority=priority, filename=filename)
131 (md5sum, size, filename) = line.split()
132 entry = dict(md5sum=md5sum, size=long(size), filename=filename)
134 entries[filename] = entry
136 for line in control["Checksums-Sha1"].split('\n'):
139 (sha1sum, size, filename) = line.split()
140 entry = entries.get(filename, None)
141 if entry.get('size', None) != long(size):
142 raise InvalidChangesException('Size for {0} in Files and Checksum-Sha1 fields differ.'.format(filename))
143 entry['sha1sum'] = sha1sum
145 for line in control["Checksums-Sha256"].split('\n'):
148 (sha256sum, size, filename) = line.split()
149 entry = entries.get(filename, None)
151 raise InvalidChangesException('No sha256sum for {0}.'.format(filename))
152 if entry.get('size', None) != long(size):
153 raise InvalidChangesException('Size for {0} in Files and Checksum-Sha256 fields differ.'.format(filename))
154 entry['sha256sum'] = sha256sum
157 for entry in entries.itervalues():
158 filename = entry['filename']
159 if 'size' not in entry:
160 raise InvalidChangesException('No size for {0}.'.format(filename))
161 if 'md5sum' not in entry:
162 raise InvalidChangesException('No md5sum for {0}.'.format(filename))
163 if 'sha1sum' not in entry:
164 raise InvalidChangesException('No sha1sum for {0}.'.format(filename))
165 if 'sha256sum' not in entry:
166 raise InvalidChangesException('No sha256sum for {0}.'.format(filename))
167 if not re_file_safe.match(filename):
168 raise InvalidChangesException("{0}: References file with unsafe filename {1}.".format(self.filename, filename))
169 f = files[filename] = HashedFile(**entry)
173 class Changes(object):
174 """Representation of a .changes file
177 architectures (list of str): list of architectures included in the upload
178 binaries (list of daklib.upload.Binary): included binary packages
179 binary_names (list of str): names of included binary packages
180 byhand_files (list of daklib.upload.HashedFile): included byhand files
181 bytes (int): total size of files included in this upload in bytes
182 changes (dict-like): dict to access fields of the .changes file
183 closed_bugs (list of str): list of bugs closed by this upload
184 directory (str): directory the .changes is located in
185 distributions (list of str): list of target distributions for the upload
186 filename (str): name of the .changes file
187 files (dict): dict mapping filenames to daklib.upload.HashedFile objects
188 path (str): path to the .changes files
189 primary_fingerprint (str): fingerprint of the PGP key used for the signature
190 source (daklib.upload.Source or None): included source
191 valid_signature (bool): True if the changes has a valid signature
193 def __init__(self, directory, filename, keyrings, require_signature=True):
194 if not re_file_safe.match(filename):
195 raise InvalidChangesException('{0}: unsafe filename'.format(filename))
196 self.directory = directory
197 self.filename = filename
198 data = open(self.path).read()
199 self._signed_file = SignedFile(data, keyrings, require_signature)
200 self.changes = apt_pkg.TagSection(self._signed_file.contents)
201 self._binaries = None
204 self._keyrings = keyrings
205 self._require_signature = require_signature
209 return os.path.join(self.directory, self.filename)
212 def primary_fingerprint(self):
213 return self._signed_file.primary_fingerprint
216 def valid_signature(self):
217 return self._signed_file.valid
220 def architectures(self):
221 return self.changes['Architecture'].split()
224 def distributions(self):
225 return self.changes['Distribution'].split()
229 if self._source is None:
231 for f in self.files.itervalues():
232 if re_file_dsc.match(f.filename) or re_file_source.match(f.filename):
233 source_files.append(f)
234 if len(source_files) > 0:
235 self._source = Source(self.directory, source_files, self._keyrings, self._require_signature)
240 if self._binaries is None:
242 for f in self.files.itervalues():
243 if re_file_binary.match(f.filename):
244 binaries.append(Binary(self.directory, f))
245 self._binaries = binaries
246 return self._binaries
249 def byhand_files(self):
252 for f in self.files.itervalues():
253 if re_file_dsc.match(f.filename) or re_file_source.match(f.filename) or re_file_binary.match(f.filename):
255 if f.section != 'byhand' and f.section[:4] != 'raw-':
256 raise InvalidChangesException("{0}: {1} looks like a byhand package, but is in section {2}".format(self.filename, f.filename, f.section))
262 def binary_names(self):
263 return self.changes['Binary'].split()
266 def closed_bugs(self):
267 return self.changes.get('Closes', '').split()
271 if self._files is None:
272 self._files = parse_file_list(self.changes, True)
278 for f in self.files.itervalues():
282 def __cmp__(self, other):
283 """Compare two changes packages
285 We sort by source name and version first. If these are identical,
286 we sort changes that include source before those without source (so
287 that sourceful uploads get processed first), and finally fall back
288 to the filename (this should really never happen).
291 -1 if self < other, 0 if self == other, 1 if self > other
293 ret = cmp(self.changes.get('Source'), other.changes.get('Source'))
297 ret = apt_pkg.version_compare(self.changes.get('Version', ''), other.changes.get('Version', ''))
300 # sort changes with source before changes without source
301 if 'source' in self.architectures and 'source' not in other.architectures:
303 elif 'source' not in self.architectures and 'source' in other.architectures:
309 # fall back to filename
310 ret = cmp(self.filename, other.filename)
314 class Binary(object):
315 """Representation of a binary package
318 component (str): component name
319 control (dict-like): dict to access fields in DEBIAN/control
320 hashed_file (HashedFile): HashedFile object for the .deb
322 def __init__(self, directory, hashed_file):
323 self.hashed_file = hashed_file
325 path = os.path.join(directory, hashed_file.filename)
326 data = apt_inst.DebFile(path).control.extractdata("control")
327 self.control = apt_pkg.TagSection(data)
331 """Get source package name and version
334 tuple containing source package name and version
336 source = self.control.get("Source", None)
338 return (self.control["Package"], self.control["Version"])
339 match = re_field_source.match(source)
341 raise InvalidBinaryException('{0}: Invalid Source field.'.format(self.hashed_file.filename))
342 version = match.group('version')
344 version = self.control['Version']
345 return (match.group('package'), version)
352 String with the package type ('deb' or 'udeb')
354 match = re_file_binary.match(self.hashed_file.filename)
356 raise InvalidBinaryException('{0}: Does not match re_file_binary'.format(self.hashed_file.filename))
357 return match.group('type')
361 fields = self.control['Section'].split('/')
366 class Source(object):
367 """Representation of a source package
370 component (str): guessed component name. Might be wrong!
371 dsc (dict-like): dict to access fields in the .dsc file
372 hashed_files (list of daklib.upload.HashedFile): list of source files (including .dsc)
373 files (dict): dictonary mapping filenames to HashedFile objects for
374 additional source files (not including .dsc)
375 primary_fingerprint (str): fingerprint of the PGP key used for the signature
376 valid_signature (bool): True if the dsc has a valid signature
378 def __init__(self, directory, hashed_files, keyrings, require_signature=True):
379 self.hashed_files = hashed_files
380 self._dsc_file = None
381 for f in hashed_files:
382 if re_file_dsc.match(f.filename):
383 if self._dsc_file is not None:
384 raise InvalidSourceException("Multiple .dsc found ({0} and {1})".format(self._dsc_file.filename, f.filename))
387 dsc_file_path = os.path.join(directory, self._dsc_file.filename)
388 data = open(dsc_file_path, 'r').read()
389 self._signed_file = SignedFile(data, keyrings, require_signature)
390 self.dsc = apt_pkg.TagSection(self._signed_file.contents)
395 if self._files is None:
396 self._files = parse_file_list(self.dsc, False)
400 def primary_fingerprint(self):
401 return self._signed_file.primary_fingerprint
404 def valid_signature(self):
405 return self._signed_file.valid
409 if 'Section' not in self.dsc:
411 fields = self.dsc['Section'].split('/')