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}:\n"
49 "According to the control file the {0} hash should be {2},\n"
52 "If you did not include {1} in you upload, a different version\n"
53 "might already be known to the archive software.") \
54 .format(self.hash_name, self.filename, self.expected, self.actual)
56 class InvalidFilenameException(Exception):
57 def __init__(self, filename):
58 self.filename = filename
60 return "Invalid filename '{0}'.".format(self.filename)
62 class HashedFile(object):
63 """file with checksums
65 def __init__(self, filename, size, md5sum, sha1sum, sha256sum, section=None, priority=None):
66 self.filename = filename
77 """MD5 hash in hexdigits
81 self.sha1sum = sha1sum
82 """SHA1 hash in hexdigits
86 self.sha256sum = sha256sum
87 """SHA256 hash in hexdigits
91 self.section = section
96 self.priority = priority
97 """priority or C{None}
102 def from_file(cls, directory, filename, section=None, priority=None):
103 """create with values for an existing file
105 Create a C{HashedFile} object that refers to an already existing file.
108 @param directory: directory the file is located in
111 @param filename: filename
113 @type section: str or C{None}
114 @param section: optional section as given in .changes files
116 @type priority: str or C{None}
117 @param priority: optional priority as given in .changes files
119 @rtype: L{HashedFile}
120 @return: C{HashedFile} object for the given file
122 path = os.path.join(directory, filename)
123 size = os.stat(path).st_size
124 with open(path, 'r') as fh:
125 hashes = apt_pkg.Hashes(fh)
126 return cls(filename, size, hashes.md5, hashes.sha1, hashes.sha256, section, priority)
128 def check(self, directory):
131 Check if size and hashes match the expected value.
134 @param directory: directory the file is located in
136 @raise InvalidHashException: hash mismatch
138 path = os.path.join(directory, self.filename)
141 size = os.stat(path).st_size
142 if size != self.size:
143 raise InvalidHashException(self.filename, 'size', self.size, size)
145 md5sum = apt_pkg.md5sum(fh)
146 if md5sum != self.md5sum:
147 raise InvalidHashException(self.filename, 'md5sum', self.md5sum, md5sum)
150 sha1sum = apt_pkg.sha1sum(fh)
151 if sha1sum != self.sha1sum:
152 raise InvalidHashException(self.filename, 'sha1sum', self.sha1sum, sha1sum)
155 sha256sum = apt_pkg.sha256sum(fh)
156 if sha256sum != self.sha256sum:
157 raise InvalidHashException(self.filename, 'sha256sum', self.sha256sum, sha256sum)
159 def parse_file_list(control, has_priority_and_section):
160 """Parse Files and Checksums-* fields
162 @type control: dict-like
163 @param control: control file to take fields from
165 @type has_priority_and_section: bool
166 @param has_priority_and_section: Files field include section and priority
169 @raise InvalidChangesException: missing fields or other grave errors
172 @return: dict mapping filenames to L{daklib.upload.HashedFile} objects
176 for line in control["Files"].split('\n'):
180 if has_priority_and_section:
181 (md5sum, size, section, priority, filename) = line.split()
182 entry = dict(md5sum=md5sum, size=long(size), section=section, priority=priority, filename=filename)
184 (md5sum, size, filename) = line.split()
185 entry = dict(md5sum=md5sum, size=long(size), filename=filename)
187 entries[filename] = entry
189 for line in control["Checksums-Sha1"].split('\n'):
192 (sha1sum, size, filename) = line.split()
193 entry = entries.get(filename, None)
194 if entry.get('size', None) != long(size):
195 raise InvalidChangesException('Size for {0} in Files and Checksum-Sha1 fields differ.'.format(filename))
196 entry['sha1sum'] = sha1sum
198 for line in control["Checksums-Sha256"].split('\n'):
201 (sha256sum, size, filename) = line.split()
202 entry = entries.get(filename, None)
204 raise InvalidChangesException('No sha256sum for {0}.'.format(filename))
205 if entry.get('size', None) != long(size):
206 raise InvalidChangesException('Size for {0} in Files and Checksum-Sha256 fields differ.'.format(filename))
207 entry['sha256sum'] = sha256sum
210 for entry in entries.itervalues():
211 filename = entry['filename']
212 if 'size' not in entry:
213 raise InvalidChangesException('No size for {0}.'.format(filename))
214 if 'md5sum' not in entry:
215 raise InvalidChangesException('No md5sum for {0}.'.format(filename))
216 if 'sha1sum' not in entry:
217 raise InvalidChangesException('No sha1sum for {0}.'.format(filename))
218 if 'sha256sum' not in entry:
219 raise InvalidChangesException('No sha256sum for {0}.'.format(filename))
220 if not re_file_safe.match(filename):
221 raise InvalidChangesException("{0}: References file with unsafe filename {1}.".format(self.filename, filename))
222 f = files[filename] = HashedFile(**entry)
226 class Changes(object):
227 """Representation of a .changes file
229 def __init__(self, directory, filename, keyrings, require_signature=True):
230 if not re_file_safe.match(filename):
231 raise InvalidChangesException('{0}: unsafe filename'.format(filename))
233 self.directory = directory
234 """directory the .changes is located in
238 self.filename = filename
239 """name of the .changes file
243 data = open(self.path).read()
244 self._signed_file = SignedFile(data, keyrings, require_signature)
245 self.changes = apt_pkg.TagSection(self._signed_file.contents)
246 """dict to access fields of the .changes file
250 self._binaries = None
253 self._keyrings = keyrings
254 self._require_signature = require_signature
258 """path to the .changes file
261 return os.path.join(self.directory, self.filename)
264 def primary_fingerprint(self):
265 """fingerprint of the key used for signing the .changes file
268 return self._signed_file.primary_fingerprint
271 def valid_signature(self):
272 """C{True} if the .changes has a valid signature
275 return self._signed_file.valid
278 def architectures(self):
279 """list of architectures included in the upload
282 return self.changes['Architecture'].split()
285 def distributions(self):
286 """list of target distributions for the upload
289 return self.changes['Distribution'].split()
293 """included source or C{None}
294 @type: L{daklib.upload.Source} or C{None}
296 if self._source is None:
298 for f in self.files.itervalues():
299 if re_file_dsc.match(f.filename) or re_file_source.match(f.filename):
300 source_files.append(f)
301 if len(source_files) > 0:
302 self._source = Source(self.directory, source_files, self._keyrings, self._require_signature)
307 """C{True} if the upload includes source
310 return "source" in self.architectures
313 def source_name(self):
314 """source package name
317 return re_field_source.match(self.changes['Source']).group('package')
321 """included binary packages
322 @type: list of L{daklib.upload.Binary}
324 if self._binaries is None:
326 for f in self.files.itervalues():
327 if re_file_binary.match(f.filename):
328 binaries.append(Binary(self.directory, f))
329 self._binaries = binaries
330 return self._binaries
333 def byhand_files(self):
334 """included byhand files
335 @type: list of L{daklib.upload.HashedFile}
339 for f in self.files.itervalues():
340 if re_file_dsc.match(f.filename) or re_file_source.match(f.filename) or re_file_binary.match(f.filename):
342 if f.section != 'byhand' and f.section[:4] != 'raw-':
343 raise InvalidChangesException("{0}: {1} looks like a byhand package, but is in section {2}".format(self.filename, f.filename, f.section))
349 def binary_names(self):
350 """names of included binary packages
353 return self.changes['Binary'].split()
356 def closed_bugs(self):
357 """bugs closed by this upload
360 return self.changes.get('Closes', '').split()
364 """dict mapping filenames to L{daklib.upload.HashedFile} objects
367 if self._files is None:
368 self._files = parse_file_list(self.changes, True)
373 """total size of files included in this upload in bytes
377 for f in self.files.itervalues():
381 def __cmp__(self, other):
382 """compare two changes files
384 We sort by source name and version first. If these are identical,
385 we sort changes that include source before those without source (so
386 that sourceful uploads get processed first), and finally fall back
387 to the filename (this should really never happen).
390 @return: n where n < 0 if self < other, n = 0 if self == other, n > 0 if self > other
392 ret = cmp(self.changes.get('Source'), other.changes.get('Source'))
396 ret = apt_pkg.version_compare(self.changes.get('Version', ''), other.changes.get('Version', ''))
399 # sort changes with source before changes without source
400 if 'source' in self.architectures and 'source' not in other.architectures:
402 elif 'source' not in self.architectures and 'source' in other.architectures:
408 # fall back to filename
409 ret = cmp(self.filename, other.filename)
413 class Binary(object):
414 """Representation of a binary package
416 def __init__(self, directory, hashed_file):
417 self.hashed_file = hashed_file
418 """file object for the .deb
422 path = os.path.join(directory, hashed_file.filename)
423 data = apt_inst.DebFile(path).control.extractdata("control")
425 self.control = apt_pkg.TagSection(data)
426 """dict to access fields in DEBIAN/control
431 def from_file(cls, directory, filename):
432 hashed_file = HashedFile.from_file(directory, filename)
433 return cls(directory, hashed_file)
437 """get tuple with source package name and version
440 source = self.control.get("Source", None)
442 return (self.control["Package"], self.control["Version"])
443 match = re_field_source.match(source)
445 raise InvalidBinaryException('{0}: Invalid Source field.'.format(self.hashed_file.filename))
446 version = match.group('version')
448 version = self.control['Version']
449 return (match.group('package'), version)
453 """package type ('deb' or 'udeb')
456 match = re_file_binary.match(self.hashed_file.filename)
458 raise InvalidBinaryException('{0}: Does not match re_file_binary'.format(self.hashed_file.filename))
459 return match.group('type')
466 fields = self.control['Section'].split('/')
471 class Source(object):
472 """Representation of a source package
474 def __init__(self, directory, hashed_files, keyrings, require_signature=True):
475 self.hashed_files = hashed_files
476 """list of source files (including the .dsc itself)
477 @type: list of L{HashedFile}
480 self._dsc_file = None
481 for f in hashed_files:
482 if re_file_dsc.match(f.filename):
483 if self._dsc_file is not None:
484 raise InvalidSourceException("Multiple .dsc found ({0} and {1})".format(self._dsc_file.filename, f.filename))
488 # make sure the hash for the dsc is valid before we use it
489 self._dsc_file.check(directory)
491 dsc_file_path = os.path.join(directory, self._dsc_file.filename)
492 data = open(dsc_file_path, 'r').read()
493 self._signed_file = SignedFile(data, keyrings, require_signature)
494 self.dsc = apt_pkg.TagSection(self._signed_file.contents)
495 """dict to access fields in the .dsc file
502 def from_file(cls, directory, filename, keyrings, require_signature=True):
503 hashed_file = HashedFile.from_file(directory, filename)
504 return cls(directory, [hashed_file], keyrings, require_signature)
508 """dict mapping filenames to L{HashedFile} objects for additional source files
510 This list does not include the .dsc itself.
514 if self._files is None:
515 self._files = parse_file_list(self.dsc, False)
519 def primary_fingerprint(self):
520 """fingerprint of the key used to sign the .dsc
523 return self._signed_file.primary_fingerprint
526 def valid_signature(self):
527 """C{True} if the .dsc has a valid signature
530 return self._signed_file.valid
534 """guessed component name
536 Might be wrong. Don't rely on this.
540 if 'Section' not in self.dsc:
542 fields = self.dsc['Section'].split('/')
549 """filename of .dsc file
552 return self._dsc_file.filename