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