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