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