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