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 is not None and 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)
203 if entry is not None and entry.get('size', None) != long(size):
204 raise InvalidChangesException('Size for {0} in Files and Checksum-Sha256 fields differ.'.format(filename))
205 entry['sha256sum'] = sha256sum
208 for entry in entries.itervalues():
209 filename = entry['filename']
210 if 'size' not in entry:
211 raise InvalidChangesException('No size for {0}.'.format(filename))
212 if 'md5sum' not in entry:
213 raise InvalidChangesException('No md5sum for {0}.'.format(filename))
214 if 'sha1sum' not in entry:
215 raise InvalidChangesException('No sha1sum for {0}.'.format(filename))
216 if 'sha256sum' not in entry:
217 raise InvalidChangesException('No sha256sum for {0}.'.format(filename))
218 if not re_file_safe.match(filename):
219 raise InvalidChangesException("{0}: References file with unsafe filename {1}.".format(self.filename, filename))
220 f = files[filename] = HashedFile(**entry)
224 class Changes(object):
225 """Representation of a .changes file
227 def __init__(self, directory, filename, keyrings, require_signature=True):
228 if not re_file_safe.match(filename):
229 raise InvalidChangesException('{0}: unsafe filename'.format(filename))
231 self.directory = directory
232 """directory the .changes is located in
236 self.filename = filename
237 """name of the .changes file
241 data = open(self.path).read()
242 self._signed_file = SignedFile(data, keyrings, require_signature)
243 self.changes = apt_pkg.TagSection(self._signed_file.contents)
244 """dict to access fields of the .changes file
248 self._binaries = None
251 self._keyrings = keyrings
252 self._require_signature = require_signature
256 """path to the .changes file
259 return os.path.join(self.directory, self.filename)
262 def primary_fingerprint(self):
263 """fingerprint of the key used for signing the .changes file
266 return self._signed_file.primary_fingerprint
269 def valid_signature(self):
270 """C{True} if the .changes has a valid signature
273 return self._signed_file.valid
276 def architectures(self):
277 """list of architectures included in the upload
280 return self.changes['Architecture'].split()
283 def distributions(self):
284 """list of target distributions for the upload
287 return self.changes['Distribution'].split()
291 """included source or C{None}
292 @type: L{daklib.upload.Source} or C{None}
294 if self._source is None:
296 for f in self.files.itervalues():
297 if re_file_dsc.match(f.filename) or re_file_source.match(f.filename):
298 source_files.append(f)
299 if len(source_files) > 0:
300 self._source = Source(self.directory, source_files, self._keyrings, self._require_signature)
305 """C{True} if the upload includes source
308 return "source" in self.architectures
311 def source_name(self):
312 """source package name
315 return re_field_source.match(self.changes['Source']).group('package')
319 """included binary packages
320 @type: list of L{daklib.upload.Binary}
322 if self._binaries is None:
324 for f in self.files.itervalues():
325 if re_file_binary.match(f.filename):
326 binaries.append(Binary(self.directory, f))
327 self._binaries = binaries
328 return self._binaries
331 def byhand_files(self):
332 """included byhand files
333 @type: list of L{daklib.upload.HashedFile}
337 for f in self.files.itervalues():
338 if re_file_dsc.match(f.filename) or re_file_source.match(f.filename) or re_file_binary.match(f.filename):
340 if f.section != 'byhand' and f.section[:4] != 'raw-':
341 raise InvalidChangesException("{0}: {1} looks like a byhand package, but is in section {2}".format(self.filename, f.filename, f.section))
347 def binary_names(self):
348 """names of included binary packages
351 return self.changes['Binary'].split()
354 def closed_bugs(self):
355 """bugs closed by this upload
358 return self.changes.get('Closes', '').split()
362 """dict mapping filenames to L{daklib.upload.HashedFile} objects
365 if self._files is None:
366 self._files = parse_file_list(self.changes, True)
371 """total size of files included in this upload in bytes
375 for f in self.files.itervalues():
379 def __cmp__(self, other):
380 """compare two changes files
382 We sort by source name and version first. If these are identical,
383 we sort changes that include source before those without source (so
384 that sourceful uploads get processed first), and finally fall back
385 to the filename (this should really never happen).
388 @return: n where n < 0 if self < other, n = 0 if self == other, n > 0 if self > other
390 ret = cmp(self.changes.get('Source'), other.changes.get('Source'))
394 ret = apt_pkg.version_compare(self.changes.get('Version', ''), other.changes.get('Version', ''))
397 # sort changes with source before changes without source
398 if 'source' in self.architectures and 'source' not in other.architectures:
400 elif 'source' not in self.architectures and 'source' in other.architectures:
406 # fall back to filename
407 ret = cmp(self.filename, other.filename)
411 class Binary(object):
412 """Representation of a binary package
414 def __init__(self, directory, hashed_file):
415 self.hashed_file = hashed_file
416 """file object for the .deb
420 path = os.path.join(directory, hashed_file.filename)
421 data = apt_inst.DebFile(path).control.extractdata("control")
423 self.control = apt_pkg.TagSection(data)
424 """dict to access fields in DEBIAN/control
429 def from_file(cls, directory, filename):
430 hashed_file = HashedFile.from_file(directory, filename)
431 return cls(directory, hashed_file)
435 """get tuple with source package name and version
438 source = self.control.get("Source", None)
440 return (self.control["Package"], self.control["Version"])
441 match = re_field_source.match(source)
443 raise InvalidBinaryException('{0}: Invalid Source field.'.format(self.hashed_file.filename))
444 version = match.group('version')
446 version = self.control['Version']
447 return (match.group('package'), version)
451 """package type ('deb' or 'udeb')
454 match = re_file_binary.match(self.hashed_file.filename)
456 raise InvalidBinaryException('{0}: Does not match re_file_binary'.format(self.hashed_file.filename))
457 return match.group('type')
464 fields = self.control['Section'].split('/')
469 class Source(object):
470 """Representation of a source package
472 def __init__(self, directory, hashed_files, keyrings, require_signature=True):
473 self.hashed_files = hashed_files
474 """list of source files (including the .dsc itself)
475 @type: list of L{HashedFile}
478 self._dsc_file = None
479 for f in hashed_files:
480 if re_file_dsc.match(f.filename):
481 if self._dsc_file is not None:
482 raise InvalidSourceException("Multiple .dsc found ({0} and {1})".format(self._dsc_file.filename, f.filename))
486 # make sure the hash for the dsc is valid before we use it
487 self._dsc_file.check(directory)
489 dsc_file_path = os.path.join(directory, self._dsc_file.filename)
490 data = open(dsc_file_path, 'r').read()
491 self._signed_file = SignedFile(data, keyrings, require_signature)
492 self.dsc = apt_pkg.TagSection(self._signed_file.contents)
493 """dict to access fields in the .dsc file
500 def from_file(cls, directory, filename, keyrings, require_signature=True):
501 hashed_file = HashedFile.from_file(directory, filename)
502 return cls(directory, [hashed_file], keyrings, require_signature)
506 """dict mapping filenames to L{HashedFile} objects for additional source files
508 This list does not include the .dsc itself.
512 if self._files is None:
513 self._files = parse_file_list(self.dsc, False)
517 def primary_fingerprint(self):
518 """fingerprint of the key used to sign the .dsc
521 return self._signed_file.primary_fingerprint
524 def valid_signature(self):
525 """C{True} if the .dsc has a valid signature
528 return self._signed_file.valid
532 """guessed component name
534 Might be wrong. Don't rely on this.
538 if 'Section' not in self.dsc:
540 fields = self.dsc['Section'].split('/')
547 """filename of .dsc file
550 return self._dsc_file.filename