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)
301 """C{True} if the upload includes source
304 return "source" in self.architectures
307 def source_name(self):
308 """source package name
311 return re_field_source.match(self.changes['Source']).group('package')
315 """included binary packages
316 @type: list of L{daklib.upload.Binary}
318 if self._binaries is None:
320 for f in self.files.itervalues():
321 if re_file_binary.match(f.filename):
322 binaries.append(Binary(self.directory, f))
323 self._binaries = binaries
324 return self._binaries
327 def byhand_files(self):
328 """included byhand files
329 @type: list of L{daklib.upload.HashedFile}
333 for f in self.files.itervalues():
334 if re_file_dsc.match(f.filename) or re_file_source.match(f.filename) or re_file_binary.match(f.filename):
336 if f.section != 'byhand' and f.section[:4] != 'raw-':
337 raise InvalidChangesException("{0}: {1} looks like a byhand package, but is in section {2}".format(self.filename, f.filename, f.section))
343 def binary_names(self):
344 """names of included binary packages
347 return self.changes['Binary'].split()
350 def closed_bugs(self):
351 """bugs closed by this upload
354 return self.changes.get('Closes', '').split()
358 """dict mapping filenames to L{daklib.upload.HashedFile} objects
361 if self._files is None:
362 self._files = parse_file_list(self.changes, True)
367 """total size of files included in this upload in bytes
371 for f in self.files.itervalues():
375 def __cmp__(self, other):
376 """compare two changes files
378 We sort by source name and version first. If these are identical,
379 we sort changes that include source before those without source (so
380 that sourceful uploads get processed first), and finally fall back
381 to the filename (this should really never happen).
384 @return: n where n < 0 if self < other, n = 0 if self == other, n > 0 if self > other
386 ret = cmp(self.changes.get('Source'), other.changes.get('Source'))
390 ret = apt_pkg.version_compare(self.changes.get('Version', ''), other.changes.get('Version', ''))
393 # sort changes with source before changes without source
394 if 'source' in self.architectures and 'source' not in other.architectures:
396 elif 'source' not in self.architectures and 'source' in other.architectures:
402 # fall back to filename
403 ret = cmp(self.filename, other.filename)
407 class Binary(object):
408 """Representation of a binary package
410 def __init__(self, directory, hashed_file):
411 self.hashed_file = hashed_file
412 """file object for the .deb
416 path = os.path.join(directory, hashed_file.filename)
417 data = apt_inst.DebFile(path).control.extractdata("control")
419 self.control = apt_pkg.TagSection(data)
420 """dict to access fields in DEBIAN/control
425 def from_file(cls, directory, filename):
426 hashed_file = HashedFile.from_file(directory, filename)
427 return cls(directory, hashed_file)
431 """get tuple with source package name and version
434 source = self.control.get("Source", None)
436 return (self.control["Package"], self.control["Version"])
437 match = re_field_source.match(source)
439 raise InvalidBinaryException('{0}: Invalid Source field.'.format(self.hashed_file.filename))
440 version = match.group('version')
442 version = self.control['Version']
443 return (match.group('package'), version)
447 """package type ('deb' or 'udeb')
450 match = re_file_binary.match(self.hashed_file.filename)
452 raise InvalidBinaryException('{0}: Does not match re_file_binary'.format(self.hashed_file.filename))
453 return match.group('type')
460 fields = self.control['Section'].split('/')
465 class Source(object):
466 """Representation of a source package
468 def __init__(self, directory, hashed_files, keyrings, require_signature=True):
469 self.hashed_files = hashed_files
470 """list of source files (including the .dsc itself)
471 @type: list of L{HashedFile}
474 self._dsc_file = None
475 for f in hashed_files:
476 if re_file_dsc.match(f.filename):
477 if self._dsc_file is not None:
478 raise InvalidSourceException("Multiple .dsc found ({0} and {1})".format(self._dsc_file.filename, f.filename))
482 # make sure the hash for the dsc is valid before we use it
483 self._dsc_file.check(directory)
485 dsc_file_path = os.path.join(directory, self._dsc_file.filename)
486 data = open(dsc_file_path, 'r').read()
487 self._signed_file = SignedFile(data, keyrings, require_signature)
488 self.dsc = apt_pkg.TagSection(self._signed_file.contents)
489 """dict to access fields in the .dsc file
496 def from_file(cls, directory, filename, keyrings, require_signature=True):
497 hashed_file = HashedFile.from_file(directory, filename)
498 return cls(directory, [hashed_file], keyrings, require_signature)
502 """dict mapping filenames to L{HashedFile} objects for additional source files
504 This list does not include the .dsc itself.
508 if self._files is None:
509 self._files = parse_file_list(self.dsc, False)
513 def primary_fingerprint(self):
514 """fingerprint of the key used to sign the .dsc
517 return self._signed_file.primary_fingerprint
520 def valid_signature(self):
521 """C{True} if the .dsc has a valid signature
524 return self._signed_file.valid
528 """guessed component name
530 Might be wrong. Don't rely on this.
534 if 'Section' not in self.dsc:
536 fields = self.dsc['Section'].split('/')
543 """filename of .dsc file
546 return self._dsc_file.filename