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