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