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