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