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 L{Changes} class which represents a changes file.
21 It provides methods to access the included binary and source packages.
29 from daklib.gpg import SignedFile
30 from daklib.regexes import *
32 class InvalidChangesException(Exception):
35 class InvalidBinaryException(Exception):
38 class InvalidSourceException(Exception):
41 class InvalidHashException(Exception):
42 def __init__(self, filename, hash_name, expected, actual):
43 self.filename = filename
44 self.hash_name = hash_name
45 self.expected = expected
48 return "Invalid {0} hash for {1}: expected {2}, but got {3}.".format(self.hash_name, self.filename, self.expected, self.actual)
50 class InvalidFilenameException(Exception):
51 def __init__(self, filename):
52 self.filename = filename
54 return "Invalid filename '{0}'.".format(self.filename)
56 class HashedFile(object):
57 """file with checksums
59 def __init__(self, filename, size, md5sum, sha1sum, sha256sum, section=None, priority=None):
60 self.filename = filename
71 """MD5 hash in hexdigits
75 self.sha1sum = sha1sum
76 """SHA1 hash in hexdigits
80 self.sha256sum = sha256sum
81 """SHA256 hash in hexdigits
85 self.section = section
90 self.priority = priority
91 """priority or C{None}
95 def check(self, directory):
98 Check if size and hashes match the expected value.
101 @param directory: directory the file is located in
103 @raise InvalidHashException: hash mismatch
105 path = os.path.join(directory, self.filename)
108 size = os.stat(path).st_size
109 if size != self.size:
110 raise InvalidHashException(self.filename, 'size', self.size, size)
112 md5sum = apt_pkg.md5sum(fh)
113 if md5sum != self.md5sum:
114 raise InvalidHashException(self.filename, 'md5sum', self.md5sum, md5sum)
117 sha1sum = apt_pkg.sha1sum(fh)
118 if sha1sum != self.sha1sum:
119 raise InvalidHashException(self.filename, 'sha1sum', self.sha1sum, sha1sum)
122 sha256sum = apt_pkg.sha256sum(fh)
123 if sha256sum != self.sha256sum:
124 raise InvalidHashException(self.filename, 'sha256sum', self.sha256sum, sha256sum)
126 def parse_file_list(control, has_priority_and_section):
127 """Parse Files and Checksums-* fields
129 @type control: dict-like
130 @param control: control file to take fields from
132 @type has_priority_and_section: bool
133 @param has_priority_and_section: Files field include section and priority
136 @raise InvalidChangesException: missing fields or other grave errors
139 @return: dict mapping filenames to L{daklib.upload.HashedFile} objects
143 for line in control["Files"].split('\n'):
147 if has_priority_and_section:
148 (md5sum, size, section, priority, filename) = line.split()
149 entry = dict(md5sum=md5sum, size=long(size), section=section, priority=priority, filename=filename)
151 (md5sum, size, filename) = line.split()
152 entry = dict(md5sum=md5sum, size=long(size), filename=filename)
154 entries[filename] = entry
156 for line in control["Checksums-Sha1"].split('\n'):
159 (sha1sum, size, filename) = line.split()
160 entry = entries.get(filename, None)
161 if entry.get('size', None) != long(size):
162 raise InvalidChangesException('Size for {0} in Files and Checksum-Sha1 fields differ.'.format(filename))
163 entry['sha1sum'] = sha1sum
165 for line in control["Checksums-Sha256"].split('\n'):
168 (sha256sum, size, filename) = line.split()
169 entry = entries.get(filename, None)
171 raise InvalidChangesException('No sha256sum for {0}.'.format(filename))
172 if entry.get('size', None) != long(size):
173 raise InvalidChangesException('Size for {0} in Files and Checksum-Sha256 fields differ.'.format(filename))
174 entry['sha256sum'] = sha256sum
177 for entry in entries.itervalues():
178 filename = entry['filename']
179 if 'size' not in entry:
180 raise InvalidChangesException('No size for {0}.'.format(filename))
181 if 'md5sum' not in entry:
182 raise InvalidChangesException('No md5sum for {0}.'.format(filename))
183 if 'sha1sum' not in entry:
184 raise InvalidChangesException('No sha1sum for {0}.'.format(filename))
185 if 'sha256sum' not in entry:
186 raise InvalidChangesException('No sha256sum for {0}.'.format(filename))
187 if not re_file_safe.match(filename):
188 raise InvalidChangesException("{0}: References file with unsafe filename {1}.".format(self.filename, filename))
189 f = files[filename] = HashedFile(**entry)
193 class Changes(object):
194 """Representation of a .changes file
196 def __init__(self, directory, filename, keyrings, require_signature=True):
197 if not re_file_safe.match(filename):
198 raise InvalidChangesException('{0}: unsafe filename'.format(filename))
200 self.directory = directory
201 """directory the .changes is located in
205 self.filename = filename
206 """name of the .changes file
210 data = open(self.path).read()
211 self._signed_file = SignedFile(data, keyrings, require_signature)
212 self.changes = apt_pkg.TagSection(self._signed_file.contents)
213 """dict to access fields of the .changes file
217 self._binaries = None
220 self._keyrings = keyrings
221 self._require_signature = require_signature
225 """path to the .changes file
228 return os.path.join(self.directory, self.filename)
231 def primary_fingerprint(self):
232 """fingerprint of the key used for signing the .changes file
235 return self._signed_file.primary_fingerprint
238 def valid_signature(self):
239 """C{True} if the .changes has a valid signature
242 return self._signed_file.valid
245 def architectures(self):
246 """list of architectures included in the upload
249 return self.changes['Architecture'].split()
252 def distributions(self):
253 """list of target distributions for the upload
256 return self.changes['Distribution'].split()
260 """included source or C{None}
261 @type: L{daklib.upload.Source} or C{None}
263 if self._source is None:
265 for f in self.files.itervalues():
266 if re_file_dsc.match(f.filename) or re_file_source.match(f.filename):
267 source_files.append(f)
268 if len(source_files) > 0:
269 self._source = Source(self.directory, source_files, self._keyrings, self._require_signature)
273 def source_name(self):
274 """source package name
277 return re_field_source.match(self.changes['Source']).group('package')
281 """included binary packages
282 @type: list of L{daklib.upload.Binary}
284 if self._binaries is None:
286 for f in self.files.itervalues():
287 if re_file_binary.match(f.filename):
288 binaries.append(Binary(self.directory, f))
289 self._binaries = binaries
290 return self._binaries
293 def byhand_files(self):
294 """included byhand files
295 @type: list of L{daklib.upload.HashedFile}
299 for f in self.files.itervalues():
300 if re_file_dsc.match(f.filename) or re_file_source.match(f.filename) or re_file_binary.match(f.filename):
302 if f.section != 'byhand' and f.section[:4] != 'raw-':
303 raise InvalidChangesException("{0}: {1} looks like a byhand package, but is in section {2}".format(self.filename, f.filename, f.section))
309 def binary_names(self):
310 """names of included binary packages
313 return self.changes['Binary'].split()
316 def closed_bugs(self):
317 """bugs closed by this upload
320 return self.changes.get('Closes', '').split()
324 """dict mapping filenames to L{daklib.upload.HashedFile} objects
327 if self._files is None:
328 self._files = parse_file_list(self.changes, True)
333 """total size of files included in this upload in bytes
337 for f in self.files.itervalues():
341 def __cmp__(self, other):
342 """compare two changes files
344 We sort by source name and version first. If these are identical,
345 we sort changes that include source before those without source (so
346 that sourceful uploads get processed first), and finally fall back
347 to the filename (this should really never happen).
350 @return: n where n < 0 if self < other, n = 0 if self == other, n > 0 if self > other
352 ret = cmp(self.changes.get('Source'), other.changes.get('Source'))
356 ret = apt_pkg.version_compare(self.changes.get('Version', ''), other.changes.get('Version', ''))
359 # sort changes with source before changes without source
360 if 'source' in self.architectures and 'source' not in other.architectures:
362 elif 'source' not in self.architectures and 'source' in other.architectures:
368 # fall back to filename
369 ret = cmp(self.filename, other.filename)
373 class Binary(object):
374 """Representation of a binary package
376 def __init__(self, directory, hashed_file):
377 self.hashed_file = hashed_file
378 """file object for the .deb
382 path = os.path.join(directory, hashed_file.filename)
383 data = apt_inst.DebFile(path).control.extractdata("control")
385 self.control = apt_pkg.TagSection(data)
386 """dict to access fields in DEBIAN/control
392 """get tuple with source package name and version
395 source = self.control.get("Source", None)
397 return (self.control["Package"], self.control["Version"])
398 match = re_field_source.match(source)
400 raise InvalidBinaryException('{0}: Invalid Source field.'.format(self.hashed_file.filename))
401 version = match.group('version')
403 version = self.control['Version']
404 return (match.group('package'), version)
408 """package type ('deb' or 'udeb')
411 match = re_file_binary.match(self.hashed_file.filename)
413 raise InvalidBinaryException('{0}: Does not match re_file_binary'.format(self.hashed_file.filename))
414 return match.group('type')
421 fields = self.control['Section'].split('/')
426 class Source(object):
427 """Representation of a source package
429 def __init__(self, directory, hashed_files, keyrings, require_signature=True):
430 self.hashed_files = hashed_files
431 """list of source files (including the .dsc itself)
432 @type: list of L{HashedFile}
435 self._dsc_file = None
436 for f in hashed_files:
437 if re_file_dsc.match(f.filename):
438 if self._dsc_file is not None:
439 raise InvalidSourceException("Multiple .dsc found ({0} and {1})".format(self._dsc_file.filename, f.filename))
443 # make sure the hash for the dsc is valid before we use it
444 self._dsc_file.check(directory)
446 dsc_file_path = os.path.join(directory, self._dsc_file.filename)
447 data = open(dsc_file_path, 'r').read()
448 self._signed_file = SignedFile(data, keyrings, require_signature)
449 self.dsc = apt_pkg.TagSection(self._signed_file.contents)
450 """dict to access fields in the .dsc file
458 """dict mapping filenames to L{HashedFile} objects for additional source files
460 This list does not include the .dsc itself.
464 if self._files is None:
465 self._files = parse_file_list(self.dsc, False)
469 def primary_fingerprint(self):
470 """fingerprint of the key used to sign the .dsc
473 return self._signed_file.primary_fingerprint
476 def valid_signature(self):
477 """C{True} if the .dsc has a valid signature
480 return self._signed_file.valid
484 """guessed component name
486 Might be wrong. Don't rely on this.
490 if 'Section' not in self.dsc:
492 fields = self.dsc['Section'].split('/')
499 """filename of .dsc file
502 return self._dsc_file.filename