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