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