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