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}
96 def from_file(cls, directory, filename, section=None, priority=None):
97 """create with values for an existing file
99 Create a C{HashedFile} object that refers to an already existing file.
102 @param directory: directory the file is located in
105 @param filename: filename
107 @type section: str or C{None}
108 @param section: optional section as given in .changes files
110 @type priority: str or C{None}
111 @param priority: optional priority as given in .changes files
113 @rtype: L{HashedFile}
114 @return: C{HashedFile} object for the given file
116 path = os.path.join(directory, filename)
117 size = os.stat(path).st_size
118 with open(path, 'r') as fh:
119 hashes = apt_pkg.Hashes(fh)
120 return cls(filename, size, hashes.md5, hashes.sha1, hashes.sha256, section, priority)
122 def check(self, directory):
125 Check if size and hashes match the expected value.
128 @param directory: directory the file is located in
130 @raise InvalidHashException: hash mismatch
132 path = os.path.join(directory, self.filename)
135 size = os.stat(path).st_size
136 if size != self.size:
137 raise InvalidHashException(self.filename, 'size', self.size, size)
139 md5sum = apt_pkg.md5sum(fh)
140 if md5sum != self.md5sum:
141 raise InvalidHashException(self.filename, 'md5sum', self.md5sum, md5sum)
144 sha1sum = apt_pkg.sha1sum(fh)
145 if sha1sum != self.sha1sum:
146 raise InvalidHashException(self.filename, 'sha1sum', self.sha1sum, sha1sum)
149 sha256sum = apt_pkg.sha256sum(fh)
150 if sha256sum != self.sha256sum:
151 raise InvalidHashException(self.filename, 'sha256sum', self.sha256sum, sha256sum)
153 def parse_file_list(control, has_priority_and_section):
154 """Parse Files and Checksums-* fields
156 @type control: dict-like
157 @param control: control file to take fields from
159 @type has_priority_and_section: bool
160 @param has_priority_and_section: Files field include section and priority
163 @raise InvalidChangesException: missing fields or other grave errors
166 @return: dict mapping filenames to L{daklib.upload.HashedFile} objects
170 for line in control["Files"].split('\n'):
174 if has_priority_and_section:
175 (md5sum, size, section, priority, filename) = line.split()
176 entry = dict(md5sum=md5sum, size=long(size), section=section, priority=priority, filename=filename)
178 (md5sum, size, filename) = line.split()
179 entry = dict(md5sum=md5sum, size=long(size), filename=filename)
181 entries[filename] = entry
183 for line in control["Checksums-Sha1"].split('\n'):
186 (sha1sum, size, filename) = line.split()
187 entry = entries.get(filename, None)
188 if entry.get('size', None) != long(size):
189 raise InvalidChangesException('Size for {0} in Files and Checksum-Sha1 fields differ.'.format(filename))
190 entry['sha1sum'] = sha1sum
192 for line in control["Checksums-Sha256"].split('\n'):
195 (sha256sum, size, filename) = line.split()
196 entry = entries.get(filename, None)
198 raise InvalidChangesException('No sha256sum for {0}.'.format(filename))
199 if entry.get('size', None) != long(size):
200 raise InvalidChangesException('Size for {0} in Files and Checksum-Sha256 fields differ.'.format(filename))
201 entry['sha256sum'] = sha256sum
204 for entry in entries.itervalues():
205 filename = entry['filename']
206 if 'size' not in entry:
207 raise InvalidChangesException('No size for {0}.'.format(filename))
208 if 'md5sum' not in entry:
209 raise InvalidChangesException('No md5sum for {0}.'.format(filename))
210 if 'sha1sum' not in entry:
211 raise InvalidChangesException('No sha1sum for {0}.'.format(filename))
212 if 'sha256sum' not in entry:
213 raise InvalidChangesException('No sha256sum for {0}.'.format(filename))
214 if not re_file_safe.match(filename):
215 raise InvalidChangesException("{0}: References file with unsafe filename {1}.".format(self.filename, filename))
216 f = files[filename] = HashedFile(**entry)
220 class Changes(object):
221 """Representation of a .changes file
223 def __init__(self, directory, filename, keyrings, require_signature=True):
224 if not re_file_safe.match(filename):
225 raise InvalidChangesException('{0}: unsafe filename'.format(filename))
227 self.directory = directory
228 """directory the .changes is located in
232 self.filename = filename
233 """name of the .changes file
237 data = open(self.path).read()
238 self._signed_file = SignedFile(data, keyrings, require_signature)
239 self.changes = apt_pkg.TagSection(self._signed_file.contents)
240 """dict to access fields of the .changes file
244 self._binaries = None
247 self._keyrings = keyrings
248 self._require_signature = require_signature
252 """path to the .changes file
255 return os.path.join(self.directory, self.filename)
258 def primary_fingerprint(self):
259 """fingerprint of the key used for signing the .changes file
262 return self._signed_file.primary_fingerprint
265 def valid_signature(self):
266 """C{True} if the .changes has a valid signature
269 return self._signed_file.valid
272 def architectures(self):
273 """list of architectures included in the upload
276 return self.changes['Architecture'].split()
279 def distributions(self):
280 """list of target distributions for the upload
283 return self.changes['Distribution'].split()
287 """included source or C{None}
288 @type: L{daklib.upload.Source} or C{None}
290 if self._source is None:
292 for f in self.files.itervalues():
293 if re_file_dsc.match(f.filename) or re_file_source.match(f.filename):
294 source_files.append(f)
295 if len(source_files) > 0:
296 self._source = Source(self.directory, source_files, self._keyrings, self._require_signature)
300 def source_name(self):
301 """source package name
304 return re_field_source.match(self.changes['Source']).group('package')
308 """included binary packages
309 @type: list of L{daklib.upload.Binary}
311 if self._binaries is None:
313 for f in self.files.itervalues():
314 if re_file_binary.match(f.filename):
315 binaries.append(Binary(self.directory, f))
316 self._binaries = binaries
317 return self._binaries
320 def byhand_files(self):
321 """included byhand files
322 @type: list of L{daklib.upload.HashedFile}
326 for f in self.files.itervalues():
327 if re_file_dsc.match(f.filename) or re_file_source.match(f.filename) or re_file_binary.match(f.filename):
329 if f.section != 'byhand' and f.section[:4] != 'raw-':
330 raise InvalidChangesException("{0}: {1} looks like a byhand package, but is in section {2}".format(self.filename, f.filename, f.section))
336 def binary_names(self):
337 """names of included binary packages
340 return self.changes['Binary'].split()
343 def closed_bugs(self):
344 """bugs closed by this upload
347 return self.changes.get('Closes', '').split()
351 """dict mapping filenames to L{daklib.upload.HashedFile} objects
354 if self._files is None:
355 self._files = parse_file_list(self.changes, True)
360 """total size of files included in this upload in bytes
364 for f in self.files.itervalues():
368 def __cmp__(self, other):
369 """compare two changes files
371 We sort by source name and version first. If these are identical,
372 we sort changes that include source before those without source (so
373 that sourceful uploads get processed first), and finally fall back
374 to the filename (this should really never happen).
377 @return: n where n < 0 if self < other, n = 0 if self == other, n > 0 if self > other
379 ret = cmp(self.changes.get('Source'), other.changes.get('Source'))
383 ret = apt_pkg.version_compare(self.changes.get('Version', ''), other.changes.get('Version', ''))
386 # sort changes with source before changes without source
387 if 'source' in self.architectures and 'source' not in other.architectures:
389 elif 'source' not in self.architectures and 'source' in other.architectures:
395 # fall back to filename
396 ret = cmp(self.filename, other.filename)
400 class Binary(object):
401 """Representation of a binary package
403 def __init__(self, directory, hashed_file):
404 self.hashed_file = hashed_file
405 """file object for the .deb
409 path = os.path.join(directory, hashed_file.filename)
410 data = apt_inst.DebFile(path).control.extractdata("control")
412 self.control = apt_pkg.TagSection(data)
413 """dict to access fields in DEBIAN/control
418 def from_file(cls, directory, filename):
419 hashed_file = HashedFile.from_file(directory, filename)
420 return cls(directory, hashed_file)
424 """get tuple with source package name and version
427 source = self.control.get("Source", None)
429 return (self.control["Package"], self.control["Version"])
430 match = re_field_source.match(source)
432 raise InvalidBinaryException('{0}: Invalid Source field.'.format(self.hashed_file.filename))
433 version = match.group('version')
435 version = self.control['Version']
436 return (match.group('package'), version)
440 """package type ('deb' or 'udeb')
443 match = re_file_binary.match(self.hashed_file.filename)
445 raise InvalidBinaryException('{0}: Does not match re_file_binary'.format(self.hashed_file.filename))
446 return match.group('type')
453 fields = self.control['Section'].split('/')
458 class Source(object):
459 """Representation of a source package
461 def __init__(self, directory, hashed_files, keyrings, require_signature=True):
462 self.hashed_files = hashed_files
463 """list of source files (including the .dsc itself)
464 @type: list of L{HashedFile}
467 self._dsc_file = None
468 for f in hashed_files:
469 if re_file_dsc.match(f.filename):
470 if self._dsc_file is not None:
471 raise InvalidSourceException("Multiple .dsc found ({0} and {1})".format(self._dsc_file.filename, f.filename))
475 # make sure the hash for the dsc is valid before we use it
476 self._dsc_file.check(directory)
478 dsc_file_path = os.path.join(directory, self._dsc_file.filename)
479 data = open(dsc_file_path, 'r').read()
480 self._signed_file = SignedFile(data, keyrings, require_signature)
481 self.dsc = apt_pkg.TagSection(self._signed_file.contents)
482 """dict to access fields in the .dsc file
489 def from_file(cls, directory, filename, keyrings, require_signature=True):
490 hashed_file = HashedFile.from_file(directory, filename)
491 return cls(directory, [hashed_file], keyrings, require_signature)
495 """dict mapping filenames to L{HashedFile} objects for additional source files
497 This list does not include the .dsc itself.
501 if self._files is None:
502 self._files = parse_file_list(self.dsc, False)
506 def primary_fingerprint(self):
507 """fingerprint of the key used to sign the .dsc
510 return self._signed_file.primary_fingerprint
513 def valid_signature(self):
514 """C{True} if the .dsc has a valid signature
517 return self._signed_file.valid
521 """guessed component name
523 Might be wrong. Don't rely on this.
527 if 'Section' not in self.dsc:
529 fields = self.dsc['Section'].split('/')
536 """filename of .dsc file
539 return self._dsc_file.filename