]> git.decadent.org.uk Git - dak.git/blob - daklib/utils.py
Contents generation should be working now
[dak.git] / daklib / utils.py
1 #!/usr/bin/env python
2 # vim:set et ts=4 sw=4:
3
4 """Utility functions
5
6 @contact: Debian FTP Master <ftpmaster@debian.org>
7 @copyright: 2000, 2001, 2002, 2003, 2004, 2005, 2006  James Troup <james@nocrew.org>
8 @license: GNU General Public License version 2 or later
9 """
10
11 # This program is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
15
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 # GNU General Public License for more details.
20
21 # You should have received a copy of the GNU General Public License
22 # along with this program; if not, write to the Free Software
23 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
24
25 import codecs
26 import commands
27 import email.Header
28 import os
29 import pwd
30 import select
31 import socket
32 import shutil
33 import sys
34 import tempfile
35 import traceback
36 import stat
37 import apt_pkg
38 import database
39 import time
40 import tarfile
41 from dak_exceptions import *
42 from regexes import re_html_escaping, html_escaping, re_single_line_field, \
43                     re_multi_line_field, re_srchasver, re_verwithext, \
44                     re_parse_maintainer, re_taint_free, re_gpg_uid
45
46 ################################################################################
47
48 default_config = "/etc/dak/dak.conf"     #: default dak config, defines host properties
49 default_apt_config = "/etc/dak/apt.conf" #: default apt config, not normally used
50
51 alias_cache = None        #: Cache for email alias checks
52 key_uid_email_cache = {}  #: Cache for email addresses from gpg key uids
53
54 # (hashname, function, earliest_changes_version)
55 known_hashes = [("sha1", apt_pkg.sha1sum, (1, 8)),
56                 ("sha256", apt_pkg.sha256sum, (1, 8))] #: hashes we accept for entries in .changes/.dsc
57
58 ################################################################################
59
60 def html_escape(s):
61     """ Escape html chars """
62     return re_html_escaping.sub(lambda x: html_escaping.get(x.group(0)), s)
63
64 ################################################################################
65
66 def open_file(filename, mode='r'):
67     """
68     Open C{file}, return fileobject.
69
70     @type filename: string
71     @param filename: path/filename to open
72
73     @type mode: string
74     @param mode: open mode
75
76     @rtype: fileobject
77     @return: open fileobject
78
79     @raise CantOpenError: If IOError is raised by open, reraise it as CantOpenError.
80
81     """
82     try:
83         f = open(filename, mode)
84     except IOError:
85         raise CantOpenError, filename
86     return f
87
88 ################################################################################
89
90 def our_raw_input(prompt=""):
91     if prompt:
92         sys.stdout.write(prompt)
93     sys.stdout.flush()
94     try:
95         ret = raw_input()
96         return ret
97     except EOFError:
98         sys.stderr.write("\nUser interrupt (^D).\n")
99         raise SystemExit
100
101 ################################################################################
102
103 def extract_component_from_section(section):
104     component = ""
105
106     if section.find('/') != -1:
107         component = section.split('/')[0]
108
109     # Expand default component
110     if component == "":
111         if Cnf.has_key("Component::%s" % section):
112             component = section
113         else:
114             component = "main"
115
116     return (section, component)
117
118 ################################################################################
119
120 def parse_deb822(contents, signing_rules=0):
121     error = ""
122     changes = {}
123
124     # Split the lines in the input, keeping the linebreaks.
125     lines = contents.splitlines(True)
126
127     if len(lines) == 0:
128         raise ParseChangesError, "[Empty changes file]"
129
130     # Reindex by line number so we can easily verify the format of
131     # .dsc files...
132     index = 0
133     indexed_lines = {}
134     for line in lines:
135         index += 1
136         indexed_lines[index] = line[:-1]
137
138     inside_signature = 0
139
140     num_of_lines = len(indexed_lines.keys())
141     index = 0
142     first = -1
143     while index < num_of_lines:
144         index += 1
145         line = indexed_lines[index]
146         if line == "":
147             if signing_rules == 1:
148                 index += 1
149                 if index > num_of_lines:
150                     raise InvalidDscError, index
151                 line = indexed_lines[index]
152                 if not line.startswith("-----BEGIN PGP SIGNATURE"):
153                     raise InvalidDscError, index
154                 inside_signature = 0
155                 break
156             else:
157                 continue
158         if line.startswith("-----BEGIN PGP SIGNATURE"):
159             break
160         if line.startswith("-----BEGIN PGP SIGNED MESSAGE"):
161             inside_signature = 1
162             if signing_rules == 1:
163                 while index < num_of_lines and line != "":
164                     index += 1
165                     line = indexed_lines[index]
166             continue
167         # If we're not inside the signed data, don't process anything
168         if signing_rules >= 0 and not inside_signature:
169             continue
170         slf = re_single_line_field.match(line)
171         if slf:
172             field = slf.groups()[0].lower()
173             changes[field] = slf.groups()[1]
174             first = 1
175             continue
176         if line == " .":
177             changes[field] += '\n'
178             continue
179         mlf = re_multi_line_field.match(line)
180         if mlf:
181             if first == -1:
182                 raise ParseChangesError, "'%s'\n [Multi-line field continuing on from nothing?]" % (line)
183             if first == 1 and changes[field] != "":
184                 changes[field] += '\n'
185             first = 0
186             changes[field] += mlf.groups()[0] + '\n'
187             continue
188         error += line
189
190     if signing_rules == 1 and inside_signature:
191         raise InvalidDscError, index
192
193     changes["filecontents"] = "".join(lines)
194
195     if changes.has_key("source"):
196         # Strip the source version in brackets from the source field,
197         # put it in the "source-version" field instead.
198         srcver = re_srchasver.search(changes["source"])
199         if srcver:
200             changes["source"] = srcver.group(1)
201             changes["source-version"] = srcver.group(2)
202
203     if error:
204         raise ParseChangesError, error
205
206     return changes
207
208 ################################################################################
209
210 def parse_changes(filename, signing_rules=0):
211     """
212     Parses a changes file and returns a dictionary where each field is a
213     key.  The mandatory first argument is the filename of the .changes
214     file.
215
216     signing_rules is an optional argument:
217
218       - If signing_rules == -1, no signature is required.
219       - If signing_rules == 0 (the default), a signature is required.
220       - If signing_rules == 1, it turns on the same strict format checking
221         as dpkg-source.
222
223     The rules for (signing_rules == 1)-mode are:
224
225       - The PGP header consists of "-----BEGIN PGP SIGNED MESSAGE-----"
226         followed by any PGP header data and must end with a blank line.
227
228       - The data section must end with a blank line and must be followed by
229         "-----BEGIN PGP SIGNATURE-----".
230     """
231
232     changes_in = open_file(filename)
233     content = changes_in.read()
234     changes_in.close()
235     return parse_deb822(content, signing_rules)
236
237 ################################################################################
238
239 def hash_key(hashname):
240     return '%ssum' % hashname
241
242 ################################################################################
243
244 def create_hash(where, files, hashname, hashfunc):
245     """
246     create_hash extends the passed files dict with the given hash by
247     iterating over all files on disk and passing them to the hashing
248     function given.
249     """
250
251     rejmsg = []
252     for f in files.keys():
253         try:
254             file_handle = open_file(f)
255         except CantOpenError:
256             rejmsg.append("Could not open file %s for checksumming" % (f))
257
258         files[f][hash_key(hashname)] = hashfunc(file_handle)
259
260         file_handle.close()
261     return rejmsg
262
263 ################################################################################
264
265 def check_hash(where, files, hashname, hashfunc):
266     """
267     check_hash checks the given hash in the files dict against the actual
268     files on disk.  The hash values need to be present consistently in
269     all file entries.  It does not modify its input in any way.
270     """
271
272     rejmsg = []
273     for f in files.keys():
274         file_handle = None
275         try:
276             try:
277                 file_handle = open_file(f)
278     
279                 # Check for the hash entry, to not trigger a KeyError.
280                 if not files[f].has_key(hash_key(hashname)):
281                     rejmsg.append("%s: misses %s checksum in %s" % (f, hashname,
282                         where))
283                     continue
284     
285                 # Actually check the hash for correctness.
286                 if hashfunc(file_handle) != files[f][hash_key(hashname)]:
287                     rejmsg.append("%s: %s check failed in %s" % (f, hashname,
288                         where))
289             except CantOpenError:
290                 # TODO: This happens when the file is in the pool.
291                 # warn("Cannot open file %s" % f)
292                 continue
293         finally:
294             if file_handle:
295                 file_handle.close()
296     return rejmsg
297
298 ################################################################################
299
300 def check_size(where, files):
301     """
302     check_size checks the file sizes in the passed files dict against the
303     files on disk.
304     """
305
306     rejmsg = []
307     for f in files.keys():
308         try:
309             entry = os.stat(f)
310         except OSError, exc:
311             if exc.errno == 2:
312                 # TODO: This happens when the file is in the pool.
313                 continue
314             raise
315
316         actual_size = entry[stat.ST_SIZE]
317         size = int(files[f]["size"])
318         if size != actual_size:
319             rejmsg.append("%s: actual file size (%s) does not match size (%s) in %s"
320                    % (f, actual_size, size, where))
321     return rejmsg
322
323 ################################################################################
324
325 def check_hash_fields(what, manifest):
326     """
327     check_hash_fields ensures that there are no checksum fields in the
328     given dict that we do not know about.
329     """
330
331     rejmsg = []
332     hashes = map(lambda x: x[0], known_hashes)
333     for field in manifest:
334         if field.startswith("checksums-"):
335             hashname = field.split("-",1)[1]
336             if hashname not in hashes:
337                 rejmsg.append("Unsupported checksum field for %s "\
338                     "in %s" % (hashname, what))
339     return rejmsg
340
341 ################################################################################
342
343 def _ensure_changes_hash(changes, format, version, files, hashname, hashfunc):
344     if format >= version:
345         # The version should contain the specified hash.
346         func = check_hash
347
348         # Import hashes from the changes
349         rejmsg = parse_checksums(".changes", files, changes, hashname)
350         if len(rejmsg) > 0:
351             return rejmsg
352     else:
353         # We need to calculate the hash because it can't possibly
354         # be in the file.
355         func = create_hash
356     return func(".changes", files, hashname, hashfunc)
357
358 # We could add the orig which might be in the pool to the files dict to
359 # access the checksums easily.
360
361 def _ensure_dsc_hash(dsc, dsc_files, hashname, hashfunc):
362     """
363     ensure_dsc_hashes' task is to ensure that each and every *present* hash
364     in the dsc is correct, i.e. identical to the changes file and if necessary
365     the pool.  The latter task is delegated to check_hash.
366     """
367
368     rejmsg = []
369     if not dsc.has_key('Checksums-%s' % (hashname,)):
370         return rejmsg
371     # Import hashes from the dsc
372     parse_checksums(".dsc", dsc_files, dsc, hashname)
373     # And check it...
374     rejmsg.extend(check_hash(".dsc", dsc_files, hashname, hashfunc))
375     return rejmsg
376
377 ################################################################################
378
379 def ensure_hashes(changes, dsc, files, dsc_files):
380     rejmsg = []
381
382     # Make sure we recognise the format of the Files: field in the .changes
383     format = changes.get("format", "0.0").split(".", 1)
384     if len(format) == 2:
385         format = int(format[0]), int(format[1])
386     else:
387         format = int(float(format[0])), 0
388
389     # We need to deal with the original changes blob, as the fields we need
390     # might not be in the changes dict serialised into the .dak anymore.
391     orig_changes = parse_deb822(changes['filecontents'])
392
393     # Copy the checksums over to the current changes dict.  This will keep
394     # the existing modifications to it intact.
395     for field in orig_changes:
396         if field.startswith('checksums-'):
397             changes[field] = orig_changes[field]
398
399     # Check for unsupported hashes
400     rejmsg.extend(check_hash_fields(".changes", changes))
401     rejmsg.extend(check_hash_fields(".dsc", dsc))
402
403     # We have to calculate the hash if we have an earlier changes version than
404     # the hash appears in rather than require it exist in the changes file
405     for hashname, hashfunc, version in known_hashes:
406         rejmsg.extend(_ensure_changes_hash(changes, format, version, files,
407             hashname, hashfunc))
408         if "source" in changes["architecture"]:
409             rejmsg.extend(_ensure_dsc_hash(dsc, dsc_files, hashname,
410                 hashfunc))
411
412     return rejmsg
413
414 def parse_checksums(where, files, manifest, hashname):
415     rejmsg = []
416     field = 'checksums-%s' % hashname
417     if not field in manifest:
418         return rejmsg
419     for line in manifest[field].split('\n'):
420         if not line:
421             break
422         checksum, size, checkfile = line.strip().split(' ')
423         if not files.has_key(checkfile):
424         # TODO: check for the file's entry in the original files dict, not
425         # the one modified by (auto)byhand and other weird stuff
426         #    rejmsg.append("%s: not present in files but in checksums-%s in %s" %
427         #        (file, hashname, where))
428             continue
429         if not files[checkfile]["size"] == size:
430             rejmsg.append("%s: size differs for files and checksums-%s entry "\
431                 "in %s" % (checkfile, hashname, where))
432             continue
433         files[checkfile][hash_key(hashname)] = checksum
434     for f in files.keys():
435         if not files[f].has_key(hash_key(hashname)):
436             rejmsg.append("%s: no entry in checksums-%s in %s" % (checkfile,
437                 hashname, where))
438     return rejmsg
439
440 ################################################################################
441
442 # Dropped support for 1.4 and ``buggy dchanges 3.4'' (?!) compared to di.pl
443
444 def build_file_list(changes, is_a_dsc=0, field="files", hashname="md5sum"):
445     files = {}
446
447     # Make sure we have a Files: field to parse...
448     if not changes.has_key(field):
449         raise NoFilesFieldError
450
451     # Make sure we recognise the format of the Files: field
452     format = re_verwithext.search(changes.get("format", "0.0"))
453     if not format:
454         raise UnknownFormatError, "%s" % (changes.get("format","0.0"))
455
456     format = format.groups()
457     if format[1] == None:
458         format = int(float(format[0])), 0, format[2]
459     else:
460         format = int(format[0]), int(format[1]), format[2]
461     if format[2] == None:
462         format = format[:2]
463
464     if is_a_dsc:
465         # format = (1,0) are the only formats we currently accept,
466         # format = (0,0) are missing format headers of which we still
467         # have some in the archive.
468         if format != (1,0) and format != (0,0):
469             raise UnknownFormatError, "%s" % (changes.get("format","0.0"))
470     else:
471         if (format < (1,5) or format > (1,8)):
472             raise UnknownFormatError, "%s" % (changes.get("format","0.0"))
473         if field != "files" and format < (1,8):
474             raise UnknownFormatError, "%s" % (changes.get("format","0.0"))
475
476     includes_section = (not is_a_dsc) and field == "files"
477
478     # Parse each entry/line:
479     for i in changes[field].split('\n'):
480         if not i:
481             break
482         s = i.split()
483         section = priority = ""
484         try:
485             if includes_section:
486                 (md5, size, section, priority, name) = s
487             else:
488                 (md5, size, name) = s
489         except ValueError:
490             raise ParseChangesError, i
491
492         if section == "":
493             section = "-"
494         if priority == "":
495             priority = "-"
496
497         (section, component) = extract_component_from_section(section)
498
499         files[name] = Dict(size=size, section=section,
500                            priority=priority, component=component)
501         files[name][hashname] = md5
502
503     return files
504
505 ################################################################################
506
507 def force_to_utf8(s):
508     """
509     Forces a string to UTF-8.  If the string isn't already UTF-8,
510     it's assumed to be ISO-8859-1.
511     """
512     try:
513         unicode(s, 'utf-8')
514         return s
515     except UnicodeError:
516         latin1_s = unicode(s,'iso8859-1')
517         return latin1_s.encode('utf-8')
518
519 def rfc2047_encode(s):
520     """
521     Encodes a (header) string per RFC2047 if necessary.  If the
522     string is neither ASCII nor UTF-8, it's assumed to be ISO-8859-1.
523     """
524     try:
525         codecs.lookup('ascii')[1](s)
526         return s
527     except UnicodeError:
528         pass
529     try:
530         codecs.lookup('utf-8')[1](s)
531         h = email.Header.Header(s, 'utf-8', 998)
532         return str(h)
533     except UnicodeError:
534         h = email.Header.Header(s, 'iso-8859-1', 998)
535         return str(h)
536
537 ################################################################################
538
539 # <Culus> 'The standard sucks, but my tool is supposed to interoperate
540 #          with it. I know - I'll fix the suckage and make things
541 #          incompatible!'
542
543 def fix_maintainer (maintainer):
544     """
545     Parses a Maintainer or Changed-By field and returns:
546       1. an RFC822 compatible version,
547       2. an RFC2047 compatible version,
548       3. the name
549       4. the email
550
551     The name is forced to UTF-8 for both 1. and 3..  If the name field
552     contains '.' or ',' (as allowed by Debian policy), 1. and 2. are
553     switched to 'email (name)' format.
554
555     """
556     maintainer = maintainer.strip()
557     if not maintainer:
558         return ('', '', '', '')
559
560     if maintainer.find("<") == -1:
561         email = maintainer
562         name = ""
563     elif (maintainer[0] == "<" and maintainer[-1:] == ">"):
564         email = maintainer[1:-1]
565         name = ""
566     else:
567         m = re_parse_maintainer.match(maintainer)
568         if not m:
569             raise ParseMaintError, "Doesn't parse as a valid Maintainer field."
570         name = m.group(1)
571         email = m.group(2)
572
573     # Get an RFC2047 compliant version of the name
574     rfc2047_name = rfc2047_encode(name)
575
576     # Force the name to be UTF-8
577     name = force_to_utf8(name)
578
579     if name.find(',') != -1 or name.find('.') != -1:
580         rfc822_maint = "%s (%s)" % (email, name)
581         rfc2047_maint = "%s (%s)" % (email, rfc2047_name)
582     else:
583         rfc822_maint = "%s <%s>" % (name, email)
584         rfc2047_maint = "%s <%s>" % (rfc2047_name, email)
585
586     if email.find("@") == -1 and email.find("buildd_") != 0:
587         raise ParseMaintError, "No @ found in email address part."
588
589     return (rfc822_maint, rfc2047_maint, name, email)
590
591 ################################################################################
592
593 def send_mail (message, filename=""):
594     """sendmail wrapper, takes _either_ a message string or a file as arguments"""
595
596     # If we've been passed a string dump it into a temporary file
597     if message:
598         (fd, filename) = tempfile.mkstemp()
599         os.write (fd, message)
600         os.close (fd)
601
602     # Invoke sendmail
603     (result, output) = commands.getstatusoutput("%s < %s" % (Cnf["Dinstall::SendmailCommand"], filename))
604     if (result != 0):
605         raise SendmailFailedError, output
606
607     # Clean up any temporary files
608     if message:
609         os.unlink (filename)
610
611 ################################################################################
612
613 def poolify (source, component):
614     if component:
615         component += '/'
616     if source[:3] == "lib":
617         return component + source[:4] + '/' + source + '/'
618     else:
619         return component + source[:1] + '/' + source + '/'
620
621 ################################################################################
622
623 def move (src, dest, overwrite = 0, perms = 0664):
624     if os.path.exists(dest) and os.path.isdir(dest):
625         dest_dir = dest
626     else:
627         dest_dir = os.path.dirname(dest)
628     if not os.path.exists(dest_dir):
629         umask = os.umask(00000)
630         os.makedirs(dest_dir, 02775)
631         os.umask(umask)
632     #print "Moving %s to %s..." % (src, dest)
633     if os.path.exists(dest) and os.path.isdir(dest):
634         dest += '/' + os.path.basename(src)
635     # Don't overwrite unless forced to
636     if os.path.exists(dest):
637         if not overwrite:
638             fubar("Can't move %s to %s - file already exists." % (src, dest))
639         else:
640             if not os.access(dest, os.W_OK):
641                 fubar("Can't move %s to %s - can't write to existing file." % (src, dest))
642     shutil.copy2(src, dest)
643     os.chmod(dest, perms)
644     os.unlink(src)
645
646 def copy (src, dest, overwrite = 0, perms = 0664):
647     if os.path.exists(dest) and os.path.isdir(dest):
648         dest_dir = dest
649     else:
650         dest_dir = os.path.dirname(dest)
651     if not os.path.exists(dest_dir):
652         umask = os.umask(00000)
653         os.makedirs(dest_dir, 02775)
654         os.umask(umask)
655     #print "Copying %s to %s..." % (src, dest)
656     if os.path.exists(dest) and os.path.isdir(dest):
657         dest += '/' + os.path.basename(src)
658     # Don't overwrite unless forced to
659     if os.path.exists(dest):
660         if not overwrite:
661             raise FileExistsError
662         else:
663             if not os.access(dest, os.W_OK):
664                 raise CantOverwriteError
665     shutil.copy2(src, dest)
666     os.chmod(dest, perms)
667
668 ################################################################################
669
670 def where_am_i ():
671     res = socket.gethostbyaddr(socket.gethostname())
672     database_hostname = Cnf.get("Config::" + res[0] + "::DatabaseHostname")
673     if database_hostname:
674         return database_hostname
675     else:
676         return res[0]
677
678 def which_conf_file ():
679     res = socket.gethostbyaddr(socket.gethostname())
680     if Cnf.get("Config::" + res[0] + "::DakConfig"):
681         return Cnf["Config::" + res[0] + "::DakConfig"]
682     else:
683         return default_config
684
685 def which_apt_conf_file ():
686     res = socket.gethostbyaddr(socket.gethostname())
687     if Cnf.get("Config::" + res[0] + "::AptConfig"):
688         return Cnf["Config::" + res[0] + "::AptConfig"]
689     else:
690         return default_apt_config
691
692 def which_alias_file():
693     hostname = socket.gethostbyaddr(socket.gethostname())[0]
694     aliasfn = '/var/lib/misc/'+hostname+'/forward-alias'
695     if os.path.exists(aliasfn):
696         return aliasfn
697     else:
698         return None
699
700 ################################################################################
701
702 # Escape characters which have meaning to SQL's regex comparison operator ('~')
703 # (woefully incomplete)
704
705 def regex_safe (s):
706     s = s.replace('+', '\\\\+')
707     s = s.replace('.', '\\\\.')
708     return s
709
710 ################################################################################
711
712 def TemplateSubst(map, filename):
713     """ Perform a substition of template """
714     templatefile = open_file(filename)
715     template = templatefile.read()
716     for x in map.keys():
717         template = template.replace(x,map[x])
718     templatefile.close()
719     return template
720
721 ################################################################################
722
723 def fubar(msg, exit_code=1):
724     sys.stderr.write("E: %s\n" % (msg))
725     sys.exit(exit_code)
726
727 def warn(msg):
728     sys.stderr.write("W: %s\n" % (msg))
729
730 ################################################################################
731
732 # Returns the user name with a laughable attempt at rfc822 conformancy
733 # (read: removing stray periods).
734 def whoami ():
735     return pwd.getpwuid(os.getuid())[4].split(',')[0].replace('.', '')
736
737 ################################################################################
738
739 def size_type (c):
740     t  = " B"
741     if c > 10240:
742         c = c / 1024
743         t = " KB"
744     if c > 10240:
745         c = c / 1024
746         t = " MB"
747     return ("%d%s" % (c, t))
748
749 ################################################################################
750
751 def cc_fix_changes (changes):
752     o = changes.get("architecture", "")
753     if o:
754         del changes["architecture"]
755     changes["architecture"] = {}
756     for j in o.split():
757         changes["architecture"][j] = 1
758
759 def changes_compare (a, b):
760     """ Sort by source name, source version, 'have source', and then by filename """
761     try:
762         a_changes = parse_changes(a)
763     except:
764         return -1
765
766     try:
767         b_changes = parse_changes(b)
768     except:
769         return 1
770
771     cc_fix_changes (a_changes)
772     cc_fix_changes (b_changes)
773
774     # Sort by source name
775     a_source = a_changes.get("source")
776     b_source = b_changes.get("source")
777     q = cmp (a_source, b_source)
778     if q:
779         return q
780
781     # Sort by source version
782     a_version = a_changes.get("version", "0")
783     b_version = b_changes.get("version", "0")
784     q = apt_pkg.VersionCompare(a_version, b_version)
785     if q:
786         return q
787
788     # Sort by 'have source'
789     a_has_source = a_changes["architecture"].get("source")
790     b_has_source = b_changes["architecture"].get("source")
791     if a_has_source and not b_has_source:
792         return -1
793     elif b_has_source and not a_has_source:
794         return 1
795
796     # Fall back to sort by filename
797     return cmp(a, b)
798
799 ################################################################################
800
801 def find_next_free (dest, too_many=100):
802     extra = 0
803     orig_dest = dest
804     while os.path.exists(dest) and extra < too_many:
805         dest = orig_dest + '.' + repr(extra)
806         extra += 1
807     if extra >= too_many:
808         raise NoFreeFilenameError
809     return dest
810
811 ################################################################################
812
813 def result_join (original, sep = '\t'):
814     resultlist = []
815     for i in xrange(len(original)):
816         if original[i] == None:
817             resultlist.append("")
818         else:
819             resultlist.append(original[i])
820     return sep.join(resultlist)
821
822 ################################################################################
823
824 def prefix_multi_line_string(str, prefix, include_blank_lines=0):
825     out = ""
826     for line in str.split('\n'):
827         line = line.strip()
828         if line or include_blank_lines:
829             out += "%s%s\n" % (prefix, line)
830     # Strip trailing new line
831     if out:
832         out = out[:-1]
833     return out
834
835 ################################################################################
836
837 def validate_changes_file_arg(filename, require_changes=1):
838     """
839     'filename' is either a .changes or .dak file.  If 'filename' is a
840     .dak file, it's changed to be the corresponding .changes file.  The
841     function then checks if the .changes file a) exists and b) is
842     readable and returns the .changes filename if so.  If there's a
843     problem, the next action depends on the option 'require_changes'
844     argument:
845
846       - If 'require_changes' == -1, errors are ignored and the .changes
847         filename is returned.
848       - If 'require_changes' == 0, a warning is given and 'None' is returned.
849       - If 'require_changes' == 1, a fatal error is raised.
850
851     """
852     error = None
853
854     orig_filename = filename
855     if filename.endswith(".dak"):
856         filename = filename[:-4]+".changes"
857
858     if not filename.endswith(".changes"):
859         error = "invalid file type; not a changes file"
860     else:
861         if not os.access(filename,os.R_OK):
862             if os.path.exists(filename):
863                 error = "permission denied"
864             else:
865                 error = "file not found"
866
867     if error:
868         if require_changes == 1:
869             fubar("%s: %s." % (orig_filename, error))
870         elif require_changes == 0:
871             warn("Skipping %s - %s" % (orig_filename, error))
872             return None
873         else: # We only care about the .dak file
874             return filename
875     else:
876         return filename
877
878 ################################################################################
879
880 def real_arch(arch):
881     return (arch != "source" and arch != "all")
882
883 ################################################################################
884
885 def join_with_commas_and(list):
886     if len(list) == 0: return "nothing"
887     if len(list) == 1: return list[0]
888     return ", ".join(list[:-1]) + " and " + list[-1]
889
890 ################################################################################
891
892 def pp_deps (deps):
893     pp_deps = []
894     for atom in deps:
895         (pkg, version, constraint) = atom
896         if constraint:
897             pp_dep = "%s (%s %s)" % (pkg, constraint, version)
898         else:
899             pp_dep = pkg
900         pp_deps.append(pp_dep)
901     return " |".join(pp_deps)
902
903 ################################################################################
904
905 def get_conf():
906     return Cnf
907
908 ################################################################################
909
910 def parse_args(Options):
911     """ Handle -a, -c and -s arguments; returns them as SQL constraints """
912     # Process suite
913     if Options["Suite"]:
914         suite_ids_list = []
915         for suite in split_args(Options["Suite"]):
916             suite_id = database.get_suite_id(suite)
917             if suite_id == -1:
918                 warn("suite '%s' not recognised." % (suite))
919             else:
920                 suite_ids_list.append(suite_id)
921         if suite_ids_list:
922             con_suites = "AND su.id IN (%s)" % ", ".join([ str(i) for i in suite_ids_list ])
923         else:
924             fubar("No valid suite given.")
925     else:
926         con_suites = ""
927
928     # Process component
929     if Options["Component"]:
930         component_ids_list = []
931         for component in split_args(Options["Component"]):
932             component_id = database.get_component_id(component)
933             if component_id == -1:
934                 warn("component '%s' not recognised." % (component))
935             else:
936                 component_ids_list.append(component_id)
937         if component_ids_list:
938             con_components = "AND c.id IN (%s)" % ", ".join([ str(i) for i in component_ids_list ])
939         else:
940             fubar("No valid component given.")
941     else:
942         con_components = ""
943
944     # Process architecture
945     con_architectures = ""
946     if Options["Architecture"]:
947         arch_ids_list = []
948         check_source = 0
949         for architecture in split_args(Options["Architecture"]):
950             if architecture == "source":
951                 check_source = 1
952             else:
953                 architecture_id = database.get_architecture_id(architecture)
954                 if architecture_id == -1:
955                     warn("architecture '%s' not recognised." % (architecture))
956                 else:
957                     arch_ids_list.append(architecture_id)
958         if arch_ids_list:
959             con_architectures = "AND a.id IN (%s)" % ", ".join([ str(i) for i in arch_ids_list ])
960         else:
961             if not check_source:
962                 fubar("No valid architecture given.")
963     else:
964         check_source = 1
965
966     return (con_suites, con_architectures, con_components, check_source)
967
968 ################################################################################
969
970 # Inspired(tm) by Bryn Keller's print_exc_plus (See
971 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52215)
972
973 def print_exc():
974     tb = sys.exc_info()[2]
975     while tb.tb_next:
976         tb = tb.tb_next
977     stack = []
978     frame = tb.tb_frame
979     while frame:
980         stack.append(frame)
981         frame = frame.f_back
982     stack.reverse()
983     traceback.print_exc()
984     for frame in stack:
985         print "\nFrame %s in %s at line %s" % (frame.f_code.co_name,
986                                              frame.f_code.co_filename,
987                                              frame.f_lineno)
988         for key, value in frame.f_locals.items():
989             print "\t%20s = " % key,
990             try:
991                 print value
992             except:
993                 print "<unable to print>"
994
995 ################################################################################
996
997 def try_with_debug(function):
998     try:
999         function()
1000     except SystemExit:
1001         raise
1002     except:
1003         print_exc()
1004
1005 ################################################################################
1006
1007 def arch_compare_sw (a, b):
1008     """
1009     Function for use in sorting lists of architectures.
1010
1011     Sorts normally except that 'source' dominates all others.
1012     """
1013
1014     if a == "source" and b == "source":
1015         return 0
1016     elif a == "source":
1017         return -1
1018     elif b == "source":
1019         return 1
1020
1021     return cmp (a, b)
1022
1023 ################################################################################
1024
1025 def split_args (s, dwim=1):
1026     """
1027     Split command line arguments which can be separated by either commas
1028     or whitespace.  If dwim is set, it will complain about string ending
1029     in comma since this usually means someone did 'dak ls -a i386, m68k
1030     foo' or something and the inevitable confusion resulting from 'm68k'
1031     being treated as an argument is undesirable.
1032     """
1033
1034     if s.find(",") == -1:
1035         return s.split()
1036     else:
1037         if s[-1:] == "," and dwim:
1038             fubar("split_args: found trailing comma, spurious space maybe?")
1039         return s.split(",")
1040
1041 ################################################################################
1042
1043 def Dict(**dict): return dict
1044
1045 ########################################
1046
1047 def gpgv_get_status_output(cmd, status_read, status_write):
1048     """
1049     Our very own version of commands.getouputstatus(), hacked to support
1050     gpgv's status fd.
1051     """
1052
1053     cmd = ['/bin/sh', '-c', cmd]
1054     p2cread, p2cwrite = os.pipe()
1055     c2pread, c2pwrite = os.pipe()
1056     errout, errin = os.pipe()
1057     pid = os.fork()
1058     if pid == 0:
1059         # Child
1060         os.close(0)
1061         os.close(1)
1062         os.dup(p2cread)
1063         os.dup(c2pwrite)
1064         os.close(2)
1065         os.dup(errin)
1066         for i in range(3, 256):
1067             if i != status_write:
1068                 try:
1069                     os.close(i)
1070                 except:
1071                     pass
1072         try:
1073             os.execvp(cmd[0], cmd)
1074         finally:
1075             os._exit(1)
1076
1077     # Parent
1078     os.close(p2cread)
1079     os.dup2(c2pread, c2pwrite)
1080     os.dup2(errout, errin)
1081
1082     output = status = ""
1083     while 1:
1084         i, o, e = select.select([c2pwrite, errin, status_read], [], [])
1085         more_data = []
1086         for fd in i:
1087             r = os.read(fd, 8196)
1088             if len(r) > 0:
1089                 more_data.append(fd)
1090                 if fd == c2pwrite or fd == errin:
1091                     output += r
1092                 elif fd == status_read:
1093                     status += r
1094                 else:
1095                     fubar("Unexpected file descriptor [%s] returned from select\n" % (fd))
1096         if not more_data:
1097             pid, exit_status = os.waitpid(pid, 0)
1098             try:
1099                 os.close(status_write)
1100                 os.close(status_read)
1101                 os.close(c2pread)
1102                 os.close(c2pwrite)
1103                 os.close(p2cwrite)
1104                 os.close(errin)
1105                 os.close(errout)
1106             except:
1107                 pass
1108             break
1109
1110     return output, status, exit_status
1111
1112 ################################################################################
1113
1114 def process_gpgv_output(status):
1115     # Process the status-fd output
1116     keywords = {}
1117     internal_error = ""
1118     for line in status.split('\n'):
1119         line = line.strip()
1120         if line == "":
1121             continue
1122         split = line.split()
1123         if len(split) < 2:
1124             internal_error += "gpgv status line is malformed (< 2 atoms) ['%s'].\n" % (line)
1125             continue
1126         (gnupg, keyword) = split[:2]
1127         if gnupg != "[GNUPG:]":
1128             internal_error += "gpgv status line is malformed (incorrect prefix '%s').\n" % (gnupg)
1129             continue
1130         args = split[2:]
1131         if keywords.has_key(keyword) and keyword not in [ "NODATA", "SIGEXPIRED", "KEYEXPIRED" ]:
1132             internal_error += "found duplicate status token ('%s').\n" % (keyword)
1133             continue
1134         else:
1135             keywords[keyword] = args
1136
1137     return (keywords, internal_error)
1138
1139 ################################################################################
1140
1141 def retrieve_key (filename, keyserver=None, keyring=None):
1142     """
1143     Retrieve the key that signed 'filename' from 'keyserver' and
1144     add it to 'keyring'.  Returns nothing on success, or an error message
1145     on error.
1146     """
1147
1148     # Defaults for keyserver and keyring
1149     if not keyserver:
1150         keyserver = Cnf["Dinstall::KeyServer"]
1151     if not keyring:
1152         keyring = Cnf.ValueList("Dinstall::GPGKeyring")[0]
1153
1154     # Ensure the filename contains no shell meta-characters or other badness
1155     if not re_taint_free.match(filename):
1156         return "%s: tainted filename" % (filename)
1157
1158     # Invoke gpgv on the file
1159     status_read, status_write = os.pipe()
1160     cmd = "gpgv --status-fd %s --keyring /dev/null %s" % (status_write, filename)
1161     (_, status, _) = gpgv_get_status_output(cmd, status_read, status_write)
1162
1163     # Process the status-fd output
1164     (keywords, internal_error) = process_gpgv_output(status)
1165     if internal_error:
1166         return internal_error
1167
1168     if not keywords.has_key("NO_PUBKEY"):
1169         return "didn't find expected NO_PUBKEY in gpgv status-fd output"
1170
1171     fingerprint = keywords["NO_PUBKEY"][0]
1172     # XXX - gpg sucks.  You can't use --secret-keyring=/dev/null as
1173     # it'll try to create a lockfile in /dev.  A better solution might
1174     # be a tempfile or something.
1175     cmd = "gpg --no-default-keyring --secret-keyring=%s --no-options" \
1176           % (Cnf["Dinstall::SigningKeyring"])
1177     cmd += " --keyring %s --keyserver %s --recv-key %s" \
1178            % (keyring, keyserver, fingerprint)
1179     (result, output) = commands.getstatusoutput(cmd)
1180     if (result != 0):
1181         return "'%s' failed with exit code %s" % (cmd, result)
1182
1183     return ""
1184
1185 ################################################################################
1186
1187 def gpg_keyring_args(keyrings=None):
1188     if not keyrings:
1189         keyrings = Cnf.ValueList("Dinstall::GPGKeyring")
1190
1191     return " ".join(["--keyring %s" % x for x in keyrings])
1192
1193 ################################################################################
1194
1195 def check_signature (sig_filename, reject, data_filename="", keyrings=None, autofetch=None):
1196     """
1197     Check the signature of a file and return the fingerprint if the
1198     signature is valid or 'None' if it's not.  The first argument is the
1199     filename whose signature should be checked.  The second argument is a
1200     reject function and is called when an error is found.  The reject()
1201     function must allow for two arguments: the first is the error message,
1202     the second is an optional prefix string.  It's possible for reject()
1203     to be called more than once during an invocation of check_signature().
1204     The third argument is optional and is the name of the files the
1205     detached signature applies to.  The fourth argument is optional and is
1206     a *list* of keyrings to use.  'autofetch' can either be None, True or
1207     False.  If None, the default behaviour specified in the config will be
1208     used.
1209     """
1210
1211     # Ensure the filename contains no shell meta-characters or other badness
1212     if not re_taint_free.match(sig_filename):
1213         reject("!!WARNING!! tainted signature filename: '%s'." % (sig_filename))
1214         return None
1215
1216     if data_filename and not re_taint_free.match(data_filename):
1217         reject("!!WARNING!! tainted data filename: '%s'." % (data_filename))
1218         return None
1219
1220     if not keyrings:
1221         keyrings = Cnf.ValueList("Dinstall::GPGKeyring")
1222
1223     # Autofetch the signing key if that's enabled
1224     if autofetch == None:
1225         autofetch = Cnf.get("Dinstall::KeyAutoFetch")
1226     if autofetch:
1227         error_msg = retrieve_key(sig_filename)
1228         if error_msg:
1229             reject(error_msg)
1230             return None
1231
1232     # Build the command line
1233     status_read, status_write = os.pipe()
1234     cmd = "gpgv --status-fd %s %s %s %s" % (
1235         status_write, gpg_keyring_args(keyrings), sig_filename, data_filename)
1236
1237     # Invoke gpgv on the file
1238     (output, status, exit_status) = gpgv_get_status_output(cmd, status_read, status_write)
1239
1240     # Process the status-fd output
1241     (keywords, internal_error) = process_gpgv_output(status)
1242
1243     # If we failed to parse the status-fd output, let's just whine and bail now
1244     if internal_error:
1245         reject("internal error while performing signature check on %s." % (sig_filename))
1246         reject(internal_error, "")
1247         reject("Please report the above errors to the Archive maintainers by replying to this mail.", "")
1248         return None
1249
1250     bad = ""
1251     # Now check for obviously bad things in the processed output
1252     if keywords.has_key("KEYREVOKED"):
1253         reject("The key used to sign %s has been revoked." % (sig_filename))
1254         bad = 1
1255     if keywords.has_key("BADSIG"):
1256         reject("bad signature on %s." % (sig_filename))
1257         bad = 1
1258     if keywords.has_key("ERRSIG") and not keywords.has_key("NO_PUBKEY"):
1259         reject("failed to check signature on %s." % (sig_filename))
1260         bad = 1
1261     if keywords.has_key("NO_PUBKEY"):
1262         args = keywords["NO_PUBKEY"]
1263         if len(args) >= 1:
1264             key = args[0]
1265         reject("The key (0x%s) used to sign %s wasn't found in the keyring(s)." % (key, sig_filename))
1266         bad = 1
1267     if keywords.has_key("BADARMOR"):
1268         reject("ASCII armour of signature was corrupt in %s." % (sig_filename))
1269         bad = 1
1270     if keywords.has_key("NODATA"):
1271         reject("no signature found in %s." % (sig_filename))
1272         bad = 1
1273     if keywords.has_key("EXPKEYSIG"):
1274         args = keywords["EXPKEYSIG"]
1275         if len(args) >= 1:
1276             key = args[0]
1277         reject("Signature made by expired key 0x%s" % (key))
1278         bad = 1
1279     if keywords.has_key("KEYEXPIRED") and not keywords.has_key("GOODSIG"):
1280         args = keywords["KEYEXPIRED"]
1281         expiredate=""
1282         if len(args) >= 1:
1283             timestamp = args[0]
1284             if timestamp.count("T") == 0:
1285                 expiredate = time.strftime("%Y-%m-%d", time.gmtime(timestamp))
1286             else:
1287                 expiredate = timestamp
1288         reject("The key used to sign %s has expired on %s" % (sig_filename, expiredate))
1289         bad = 1
1290
1291     if bad:
1292         return None
1293
1294     # Next check gpgv exited with a zero return code
1295     if exit_status:
1296         reject("gpgv failed while checking %s." % (sig_filename))
1297         if status.strip():
1298             reject(prefix_multi_line_string(status, " [GPG status-fd output:] "), "")
1299         else:
1300             reject(prefix_multi_line_string(output, " [GPG output:] "), "")
1301         return None
1302
1303     # Sanity check the good stuff we expect
1304     if not keywords.has_key("VALIDSIG"):
1305         reject("signature on %s does not appear to be valid [No VALIDSIG]." % (sig_filename))
1306         bad = 1
1307     else:
1308         args = keywords["VALIDSIG"]
1309         if len(args) < 1:
1310             reject("internal error while checking signature on %s." % (sig_filename))
1311             bad = 1
1312         else:
1313             fingerprint = args[0]
1314     if not keywords.has_key("GOODSIG"):
1315         reject("signature on %s does not appear to be valid [No GOODSIG]." % (sig_filename))
1316         bad = 1
1317     if not keywords.has_key("SIG_ID"):
1318         reject("signature on %s does not appear to be valid [No SIG_ID]." % (sig_filename))
1319         bad = 1
1320
1321     # Finally ensure there's not something we don't recognise
1322     known_keywords = Dict(VALIDSIG="",SIG_ID="",GOODSIG="",BADSIG="",ERRSIG="",
1323                           SIGEXPIRED="",KEYREVOKED="",NO_PUBKEY="",BADARMOR="",
1324                           NODATA="",NOTATION_DATA="",NOTATION_NAME="",KEYEXPIRED="")
1325
1326     for keyword in keywords.keys():
1327         if not known_keywords.has_key(keyword):
1328             reject("found unknown status token '%s' from gpgv with args '%r' in %s." % (keyword, keywords[keyword], sig_filename))
1329             bad = 1
1330
1331     if bad:
1332         return None
1333     else:
1334         return fingerprint
1335
1336 ################################################################################
1337
1338 def gpg_get_key_addresses(fingerprint):
1339     """retreive email addresses from gpg key uids for a given fingerprint"""
1340     addresses = key_uid_email_cache.get(fingerprint)
1341     if addresses != None:
1342         return addresses
1343     addresses = set()
1344     cmd = "gpg --no-default-keyring %s --fingerprint %s" \
1345                 % (gpg_keyring_args(), fingerprint)
1346     (result, output) = commands.getstatusoutput(cmd)
1347     if result == 0:
1348         for l in output.split('\n'):
1349             m = re_gpg_uid.match(l)
1350             if m:
1351                 addresses.add(m.group(1))
1352     key_uid_email_cache[fingerprint] = addresses
1353     return addresses
1354
1355 ################################################################################
1356
1357 # Inspired(tm) by http://www.zopelabs.com/cookbook/1022242603
1358
1359 def wrap(paragraph, max_length, prefix=""):
1360     line = ""
1361     s = ""
1362     have_started = 0
1363     words = paragraph.split()
1364
1365     for word in words:
1366         word_size = len(word)
1367         if word_size > max_length:
1368             if have_started:
1369                 s += line + '\n' + prefix
1370             s += word + '\n' + prefix
1371         else:
1372             if have_started:
1373                 new_length = len(line) + word_size + 1
1374                 if new_length > max_length:
1375                     s += line + '\n' + prefix
1376                     line = word
1377                 else:
1378                     line += ' ' + word
1379             else:
1380                 line = word
1381         have_started = 1
1382
1383     if have_started:
1384         s += line
1385
1386     return s
1387
1388 ################################################################################
1389
1390 def clean_symlink (src, dest, root):
1391     """
1392     Relativize an absolute symlink from 'src' -> 'dest' relative to 'root'.
1393     Returns fixed 'src'
1394     """
1395     src = src.replace(root, '', 1)
1396     dest = dest.replace(root, '', 1)
1397     dest = os.path.dirname(dest)
1398     new_src = '../' * len(dest.split('/'))
1399     return new_src + src
1400
1401 ################################################################################
1402
1403 def temp_filename(directory=None, prefix="dak", suffix=""):
1404     """
1405     Return a secure and unique filename by pre-creating it.
1406     If 'directory' is non-null, it will be the directory the file is pre-created in.
1407     If 'prefix' is non-null, the filename will be prefixed with it, default is dak.
1408     If 'suffix' is non-null, the filename will end with it.
1409
1410     Returns a pair (fd, name).
1411     """
1412
1413     return tempfile.mkstemp(suffix, prefix, directory)
1414
1415 ################################################################################
1416
1417 def is_email_alias(email):
1418     """ checks if the user part of the email is listed in the alias file """
1419     global alias_cache
1420     if alias_cache == None:
1421         aliasfn = which_alias_file()
1422         alias_cache = set()
1423         if aliasfn:
1424             for l in open(aliasfn):
1425                 alias_cache.add(l.split(':')[0])
1426     uid = email.split('@')[0]
1427     return uid in alias_cache
1428
1429 ################################################################################
1430
1431 apt_pkg.init()
1432
1433 Cnf = apt_pkg.newConfiguration()
1434 apt_pkg.ReadConfigFileISC(Cnf,default_config)
1435
1436 if which_conf_file() != default_config:
1437     apt_pkg.ReadConfigFileISC(Cnf,which_conf_file())
1438
1439 ################################################################################
1440
1441 def generate_contents_information(filename):
1442     """
1443     Generate a list of flies contained in a .deb
1444
1445     @type filename: string
1446     @param filename: the path to a data.tar.gz or data.tar.bz2
1447
1448     @rtype: list
1449     @return: a list of files in the data.tar.* portion of the .deb
1450     """
1451     cmd = "ar t %s" % (filename)
1452     (result, output) = commands.getstatusoutput(cmd)
1453     if result != 0:
1454         reject("%s: 'ar t' invocation failed." % (filename))
1455         reject(utils.prefix_multi_line_string(output, " [ar output:] "), "")
1456
1457     # Ugh ... this is ugly ... Code ripped from process_unchecked.py
1458     chunks = output.split('\n')
1459
1460     contents = []
1461     try:
1462         cmd = "ar x %s %s" % (filename, chunks[2])
1463         (result, output) = commands.getstatusoutput(cmd)
1464         if result != 0:
1465             reject("%s: '%s' invocation failed." % (filename, cmd))
1466             reject(utils.prefix_multi_line_string(output, " [ar output:] "), "")
1467
1468         # Got deb tarballs, now lets go through and determine what bits
1469         # and pieces the deb had ...
1470         if chunks[2] == "data.tar.gz":
1471             data = tarfile.open("data.tar.gz", "r:gz")
1472         elif chunks[2] == "data.tar.bz2":
1473             data = tarfile.open("data.tar.bz2", "r:bz2")
1474         else:
1475             os.remove(chunks[2])
1476             reject("couldn't find data.tar.*")
1477
1478         for tarinfo in data:
1479             if not tarinfo.isdir():
1480                 contents.append(tarinfo.name[2:])
1481
1482     finally:
1483         if os.path.exists( chunks[2] ):
1484             shutil.rmtree( chunks[2] )
1485             os.remove( chunks[2] )
1486
1487     return contents
1488
1489 ###############################################################################