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