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