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