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