]> git.decadent.org.uk Git - dak.git/blob - daklib/utils.py
Stop using undefined reject function in check_dsc_files
[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 codecs
27 import datetime
28 import email.Header
29 import os
30 import pwd
31 import grp
32 import select
33 import socket
34 import shutil
35 import sys
36 import tempfile
37 import traceback
38 import stat
39 import apt_inst
40 import apt_pkg
41 import time
42 import re
43 import email as modemail
44 import subprocess
45 import ldap
46 import errno
47
48 import daklib.config as config
49 import daklib.daksubprocess
50 from dbconn import DBConn, get_architecture, get_component, get_suite, \
51                    get_override_type, Keyring, session_wrapper, \
52                    get_active_keyring_paths, \
53                    get_suite_architectures, get_or_set_metadatakey, DBSource, \
54                    Component, Override, OverrideType
55 from sqlalchemy import desc
56 from dak_exceptions import *
57 from gpg import SignedFile
58 from textutils import fix_maintainer
59 from regexes import re_html_escaping, html_escaping, re_single_line_field, \
60                     re_multi_line_field, re_srchasver, re_taint_free, \
61                     re_re_mark, re_whitespace_comment, re_issource, \
62                     re_build_dep_arch, re_parse_maintainer
63
64 from formats import parse_format, validate_changes_format
65 from srcformats import get_format_from_string
66 from collections import defaultdict
67
68 ################################################################################
69
70 default_config = "/etc/dak/dak.conf"     #: default dak config, defines host properties
71
72 alias_cache = None        #: Cache for email alias checks
73 key_uid_email_cache = {}  #: Cache for email addresses from gpg key uids
74
75 # (hashname, function, earliest_changes_version)
76 known_hashes = [("sha1", apt_pkg.sha1sum, (1, 8)),
77                 ("sha256", apt_pkg.sha256sum, (1, 8))] #: hashes we accept for entries in .changes/.dsc
78
79 # Monkeypatch commands.getstatusoutput as it may not return the correct exit
80 # code in lenny's Python. This also affects commands.getoutput and
81 # commands.getstatus.
82 def dak_getstatusoutput(cmd):
83     pipe = daklib.daksubprocess.Popen(cmd, shell=True, universal_newlines=True,
84         stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
85
86     output = pipe.stdout.read()
87
88     pipe.wait()
89
90     if output[-1:] == '\n':
91         output = output[:-1]
92
93     ret = pipe.wait()
94     if ret is None:
95         ret = 0
96
97     return ret, output
98 commands.getstatusoutput = dak_getstatusoutput
99
100 ################################################################################
101
102 def html_escape(s):
103     """ Escape html chars """
104     return re_html_escaping.sub(lambda x: html_escaping.get(x.group(0)), s)
105
106 ################################################################################
107
108 def open_file(filename, mode='r'):
109     """
110     Open C{file}, return fileobject.
111
112     @type filename: string
113     @param filename: path/filename to open
114
115     @type mode: string
116     @param mode: open mode
117
118     @rtype: fileobject
119     @return: open fileobject
120
121     @raise CantOpenError: If IOError is raised by open, reraise it as CantOpenError.
122
123     """
124     try:
125         f = open(filename, mode)
126     except IOError:
127         raise CantOpenError(filename)
128     return f
129
130 ################################################################################
131
132 def our_raw_input(prompt=""):
133     if prompt:
134         while 1:
135             try:
136                 sys.stdout.write(prompt)
137                 break
138             except IOError:
139                 pass
140     sys.stdout.flush()
141     try:
142         ret = raw_input()
143         return ret
144     except EOFError:
145         sys.stderr.write("\nUser interrupt (^D).\n")
146         raise SystemExit
147
148 ################################################################################
149
150 def extract_component_from_section(section, session=None):
151     component = ""
152
153     if section.find('/') != -1:
154         component = section.split('/')[0]
155
156     # Expand default component
157     if component == "":
158         component = "main"
159
160     return (section, component)
161
162 ################################################################################
163
164 def parse_deb822(armored_contents, signing_rules=0, keyrings=None, session=None):
165     require_signature = True
166     if keyrings == None:
167         keyrings = []
168         require_signature = False
169
170     signed_file = SignedFile(armored_contents, keyrings=keyrings, require_signature=require_signature)
171     contents = signed_file.contents
172
173     error = ""
174     changes = {}
175
176     # Split the lines in the input, keeping the linebreaks.
177     lines = contents.splitlines(True)
178
179     if len(lines) == 0:
180         raise ParseChangesError("[Empty changes file]")
181
182     # Reindex by line number so we can easily verify the format of
183     # .dsc files...
184     index = 0
185     indexed_lines = {}
186     for line in lines:
187         index += 1
188         indexed_lines[index] = line[:-1]
189
190     num_of_lines = len(indexed_lines.keys())
191     index = 0
192     first = -1
193     while index < num_of_lines:
194         index += 1
195         line = indexed_lines[index]
196         if line == "" and signing_rules == 1:
197             if index != num_of_lines:
198                 raise InvalidDscError(index)
199             break
200         slf = re_single_line_field.match(line)
201         if slf:
202             field = slf.groups()[0].lower()
203             changes[field] = slf.groups()[1]
204             first = 1
205             continue
206         if line == " .":
207             changes[field] += '\n'
208             continue
209         mlf = re_multi_line_field.match(line)
210         if mlf:
211             if first == -1:
212                 raise ParseChangesError("'%s'\n [Multi-line field continuing on from nothing?]" % (line))
213             if first == 1 and changes[field] != "":
214                 changes[field] += '\n'
215             first = 0
216             changes[field] += mlf.groups()[0] + '\n'
217             continue
218         error += line
219
220     changes["filecontents"] = armored_contents
221
222     if changes.has_key("source"):
223         # Strip the source version in brackets from the source field,
224         # put it in the "source-version" field instead.
225         srcver = re_srchasver.search(changes["source"])
226         if srcver:
227             changes["source"] = srcver.group(1)
228             changes["source-version"] = srcver.group(2)
229
230     if error:
231         raise ParseChangesError(error)
232
233     return changes
234
235 ################################################################################
236
237 def parse_changes(filename, signing_rules=0, dsc_file=0, keyrings=None):
238     """
239     Parses a changes file and returns a dictionary where each field is a
240     key.  The mandatory first argument is the filename of the .changes
241     file.
242
243     signing_rules is an optional argument:
244
245       - If signing_rules == -1, no signature is required.
246       - If signing_rules == 0 (the default), a signature is required.
247       - If signing_rules == 1, it turns on the same strict format checking
248         as dpkg-source.
249
250     The rules for (signing_rules == 1)-mode are:
251
252       - The PGP header consists of "-----BEGIN PGP SIGNED MESSAGE-----"
253         followed by any PGP header data and must end with a blank line.
254
255       - The data section must end with a blank line and must be followed by
256         "-----BEGIN PGP SIGNATURE-----".
257     """
258
259     with open_file(filename) as changes_in:
260         content = changes_in.read()
261     try:
262         unicode(content, 'utf-8')
263     except UnicodeError:
264         raise ChangesUnicodeError("Changes file not proper utf-8")
265     changes = parse_deb822(content, signing_rules, keyrings=keyrings)
266
267
268     if not dsc_file:
269         # Finally ensure that everything needed for .changes is there
270         must_keywords = ('Format', 'Date', 'Source', 'Binary', 'Architecture', 'Version',
271                          'Distribution', 'Maintainer', 'Description', 'Changes', 'Files')
272
273         missingfields=[]
274         for keyword in must_keywords:
275             if not changes.has_key(keyword.lower()):
276                 missingfields.append(keyword)
277
278                 if len(missingfields):
279                     raise ParseChangesError("Missing mandatory field(s) in changes file (policy 5.5): %s" % (missingfields))
280
281     return changes
282
283 ################################################################################
284
285 def hash_key(hashname):
286     return '%ssum' % hashname
287
288 ################################################################################
289
290 def check_dsc_files(dsc_filename, dsc, dsc_files):
291     """
292     Verify that the files listed in the Files field of the .dsc are
293     those expected given the announced Format.
294
295     @type dsc_filename: string
296     @param dsc_filename: path of .dsc file
297
298     @type dsc: dict
299     @param dsc: the content of the .dsc parsed by C{parse_changes()}
300
301     @type dsc_files: dict
302     @param dsc_files: the file list returned by C{build_file_list()}
303
304     @rtype: list
305     @return: all errors detected
306     """
307     rejmsg = []
308
309     # Ensure .dsc lists proper set of source files according to the format
310     # announced
311     has = defaultdict(lambda: 0)
312
313     ftype_lookup = (
314         (r'orig\.tar\.gz',             ('orig_tar_gz', 'orig_tar')),
315         (r'diff\.gz',                  ('debian_diff',)),
316         (r'tar\.gz',                   ('native_tar_gz', 'native_tar')),
317         (r'debian\.tar\.(gz|bz2|xz)',  ('debian_tar',)),
318         (r'orig\.tar\.(gz|bz2|xz)',    ('orig_tar',)),
319         (r'orig\.tar\.(gz|bz2|xz)\.asc', ('orig_tar_sig',)),
320         (r'tar\.(gz|bz2|xz)',          ('native_tar',)),
321         (r'orig-.+\.tar\.(gz|bz2|xz)', ('more_orig_tar',)),
322         (r'orig-.+\.tar\.(gz|bz2|xz)\.asc', ('more_orig_tar_sig',)),
323     )
324
325     for f in dsc_files:
326         m = re_issource.match(f)
327         if not m:
328             rejmsg.append("%s: %s in Files field not recognised as source."
329                           % (dsc_filename, f))
330             continue
331
332         # Populate 'has' dictionary by resolving keys in lookup table
333         matched = False
334         for regex, keys in ftype_lookup:
335             if re.match(regex, m.group(3)):
336                 matched = True
337                 for key in keys:
338                     has[key] += 1
339                 break
340
341         # File does not match anything in lookup table; reject
342         if not matched:
343             rejmsg.append("%s: unexpected source file '%s'" % (dsc_filename, f))
344             break
345
346     # Check for multiple files
347     for file_type in ('orig_tar', 'orig_tar_sig', 'native_tar', 'debian_tar', 'debian_diff'):
348         if has[file_type] > 1:
349             rejmsg.append("%s: lists multiple %s" % (dsc_filename, file_type))
350
351     # Source format specific tests
352     try:
353         format = get_format_from_string(dsc['format'])
354         rejmsg.extend([
355             '%s: %s' % (dsc_filename, x) for x in format.reject_msgs(has)
356         ])
357
358     except UnknownFormatError:
359         # Not an error here for now
360         pass
361
362     return rejmsg
363
364 ################################################################################
365
366 # Dropped support for 1.4 and ``buggy dchanges 3.4'' (?!) compared to di.pl
367
368 def build_file_list(changes, is_a_dsc=0, field="files", hashname="md5sum"):
369     files = {}
370
371     # Make sure we have a Files: field to parse...
372     if not changes.has_key(field):
373         raise NoFilesFieldError
374
375     # Validate .changes Format: field
376     if not is_a_dsc:
377         validate_changes_format(parse_format(changes['format']), field)
378
379     includes_section = (not is_a_dsc) and field == "files"
380
381     # Parse each entry/line:
382     for i in changes[field].split('\n'):
383         if not i:
384             break
385         s = i.split()
386         section = priority = ""
387         try:
388             if includes_section:
389                 (md5, size, section, priority, name) = s
390             else:
391                 (md5, size, name) = s
392         except ValueError:
393             raise ParseChangesError(i)
394
395         if section == "":
396             section = "-"
397         if priority == "":
398             priority = "-"
399
400         (section, component) = extract_component_from_section(section)
401
402         files[name] = dict(size=size, section=section,
403                            priority=priority, component=component)
404         files[name][hashname] = md5
405
406     return files
407
408 ################################################################################
409
410 def send_mail (message, filename="", whitelists=None):
411     """sendmail wrapper, takes _either_ a message string or a file as arguments
412
413     @type  whitelists: list of (str or None)
414     @param whitelists: path to whitelists. C{None} or an empty list whitelists
415                        everything, otherwise an address is whitelisted if it is
416                        included in any of the lists.
417                        In addition a global whitelist can be specified in
418                        Dinstall::MailWhiteList.
419     """
420
421     maildir = Cnf.get('Dir::Mail')
422     if maildir:
423         path = os.path.join(maildir, datetime.datetime.now().isoformat())
424         path = find_next_free(path)
425         with open(path, 'w') as fh:
426             print >>fh, message,
427
428     # Check whether we're supposed to be sending mail
429     if Cnf.has_key("Dinstall::Options::No-Mail") and Cnf["Dinstall::Options::No-Mail"]:
430         return
431
432     # If we've been passed a string dump it into a temporary file
433     if message:
434         (fd, filename) = tempfile.mkstemp()
435         os.write (fd, message)
436         os.close (fd)
437
438     if whitelists is None or None in whitelists:
439         whitelists = []
440     if Cnf.get('Dinstall::MailWhiteList', ''):
441         whitelists.append(Cnf['Dinstall::MailWhiteList'])
442     if len(whitelists) != 0:
443         with open_file(filename) as message_in:
444             message_raw = modemail.message_from_file(message_in)
445
446         whitelist = [];
447         for path in whitelists:
448           with open_file(path, 'r') as whitelist_in:
449             for line in whitelist_in:
450                 if not re_whitespace_comment.match(line):
451                     if re_re_mark.match(line):
452                         whitelist.append(re.compile(re_re_mark.sub("", line.strip(), 1)))
453                     else:
454                         whitelist.append(re.compile(re.escape(line.strip())))
455
456         # Fields to check.
457         fields = ["To", "Bcc", "Cc"]
458         for field in fields:
459             # Check each field
460             value = message_raw.get(field, None)
461             if value != None:
462                 match = [];
463                 for item in value.split(","):
464                     (rfc822_maint, rfc2047_maint, name, email) = fix_maintainer(item.strip())
465                     mail_whitelisted = 0
466                     for wr in whitelist:
467                         if wr.match(email):
468                             mail_whitelisted = 1
469                             break
470                     if not mail_whitelisted:
471                         print "Skipping {0} since it's not whitelisted".format(item)
472                         continue
473                     match.append(item)
474
475                 # Doesn't have any mail in whitelist so remove the header
476                 if len(match) == 0:
477                     del message_raw[field]
478                 else:
479                     message_raw.replace_header(field, ', '.join(match))
480
481         # Change message fields in order if we don't have a To header
482         if not message_raw.has_key("To"):
483             fields.reverse()
484             for field in fields:
485                 if message_raw.has_key(field):
486                     message_raw[fields[-1]] = message_raw[field]
487                     del message_raw[field]
488                     break
489             else:
490                 # Clean up any temporary files
491                 # and return, as we removed all recipients.
492                 if message:
493                     os.unlink (filename);
494                 return;
495
496         fd = os.open(filename, os.O_RDWR|os.O_EXCL, 0o700);
497         os.write (fd, message_raw.as_string(True));
498         os.close (fd);
499
500     # Invoke sendmail
501     (result, output) = commands.getstatusoutput("%s < %s" % (Cnf["Dinstall::SendmailCommand"], filename))
502     if (result != 0):
503         raise SendmailFailedError(output)
504
505     # Clean up any temporary files
506     if message:
507         os.unlink (filename)
508
509 ################################################################################
510
511 def poolify (source, component=None):
512     if source[:3] == "lib":
513         return source[:4] + '/' + source + '/'
514     else:
515         return source[:1] + '/' + source + '/'
516
517 ################################################################################
518
519 def move (src, dest, overwrite = 0, perms = 0o664):
520     if os.path.exists(dest) and os.path.isdir(dest):
521         dest_dir = dest
522     else:
523         dest_dir = os.path.dirname(dest)
524     if not os.path.lexists(dest_dir):
525         umask = os.umask(00000)
526         os.makedirs(dest_dir, 0o2775)
527         os.umask(umask)
528     #print "Moving %s to %s..." % (src, dest)
529     if os.path.exists(dest) and os.path.isdir(dest):
530         dest += '/' + os.path.basename(src)
531     # Don't overwrite unless forced to
532     if os.path.lexists(dest):
533         if not overwrite:
534             fubar("Can't move %s to %s - file already exists." % (src, dest))
535         else:
536             if not os.access(dest, os.W_OK):
537                 fubar("Can't move %s to %s - can't write to existing file." % (src, dest))
538     shutil.copy2(src, dest)
539     os.chmod(dest, perms)
540     os.unlink(src)
541
542 def copy (src, dest, overwrite = 0, perms = 0o664):
543     if os.path.exists(dest) and os.path.isdir(dest):
544         dest_dir = dest
545     else:
546         dest_dir = os.path.dirname(dest)
547     if not os.path.exists(dest_dir):
548         umask = os.umask(00000)
549         os.makedirs(dest_dir, 0o2775)
550         os.umask(umask)
551     #print "Copying %s to %s..." % (src, dest)
552     if os.path.exists(dest) and os.path.isdir(dest):
553         dest += '/' + os.path.basename(src)
554     # Don't overwrite unless forced to
555     if os.path.lexists(dest):
556         if not overwrite:
557             raise FileExistsError
558         else:
559             if not os.access(dest, os.W_OK):
560                 raise CantOverwriteError
561     shutil.copy2(src, dest)
562     os.chmod(dest, perms)
563
564 ################################################################################
565
566 def which_conf_file ():
567     if os.getenv('DAK_CONFIG'):
568         return os.getenv('DAK_CONFIG')
569
570     res = socket.getfqdn()
571     # In case we allow local config files per user, try if one exists
572     if Cnf.find_b("Config::" + res + "::AllowLocalConfig"):
573         homedir = os.getenv("HOME")
574         confpath = os.path.join(homedir, "/etc/dak.conf")
575         if os.path.exists(confpath):
576             apt_pkg.read_config_file_isc(Cnf,confpath)
577
578     # We are still in here, so there is no local config file or we do
579     # not allow local files. Do the normal stuff.
580     if Cnf.get("Config::" + res + "::DakConfig"):
581         return Cnf["Config::" + res + "::DakConfig"]
582
583     return default_config
584
585 ################################################################################
586
587 def TemplateSubst(subst_map, filename):
588     """ Perform a substition of template """
589     with open_file(filename) as templatefile:
590         template = templatefile.read()
591     for k, v in subst_map.iteritems():
592         template = template.replace(k, str(v))
593     return template
594
595 ################################################################################
596
597 def fubar(msg, exit_code=1):
598     sys.stderr.write("E: %s\n" % (msg))
599     sys.exit(exit_code)
600
601 def warn(msg):
602     sys.stderr.write("W: %s\n" % (msg))
603
604 ################################################################################
605
606 # Returns the user name with a laughable attempt at rfc822 conformancy
607 # (read: removing stray periods).
608 def whoami ():
609     return pwd.getpwuid(os.getuid())[4].split(',')[0].replace('.', '')
610
611 def getusername ():
612     return pwd.getpwuid(os.getuid())[0]
613
614 ################################################################################
615
616 def size_type (c):
617     t  = " B"
618     if c > 10240:
619         c = c / 1024
620         t = " KB"
621     if c > 10240:
622         c = c / 1024
623         t = " MB"
624     return ("%d%s" % (c, t))
625
626 ################################################################################
627
628 def find_next_free (dest, too_many=100):
629     extra = 0
630     orig_dest = dest
631     while os.path.lexists(dest) and extra < too_many:
632         dest = orig_dest + '.' + repr(extra)
633         extra += 1
634     if extra >= too_many:
635         raise NoFreeFilenameError
636     return dest
637
638 ################################################################################
639
640 def result_join (original, sep = '\t'):
641     resultlist = []
642     for i in xrange(len(original)):
643         if original[i] == None:
644             resultlist.append("")
645         else:
646             resultlist.append(original[i])
647     return sep.join(resultlist)
648
649 ################################################################################
650
651 def prefix_multi_line_string(str, prefix, include_blank_lines=0):
652     out = ""
653     for line in str.split('\n'):
654         line = line.strip()
655         if line or include_blank_lines:
656             out += "%s%s\n" % (prefix, line)
657     # Strip trailing new line
658     if out:
659         out = out[:-1]
660     return out
661
662 ################################################################################
663
664 def join_with_commas_and(list):
665     if len(list) == 0: return "nothing"
666     if len(list) == 1: return list[0]
667     return ", ".join(list[:-1]) + " and " + list[-1]
668
669 ################################################################################
670
671 def pp_deps (deps):
672     pp_deps = []
673     for atom in deps:
674         (pkg, version, constraint) = atom
675         if constraint:
676             pp_dep = "%s (%s %s)" % (pkg, constraint, version)
677         else:
678             pp_dep = pkg
679         pp_deps.append(pp_dep)
680     return " |".join(pp_deps)
681
682 ################################################################################
683
684 def get_conf():
685     return Cnf
686
687 ################################################################################
688
689 def parse_args(Options):
690     """ Handle -a, -c and -s arguments; returns them as SQL constraints """
691     # XXX: This should go away and everything which calls it be converted
692     #      to use SQLA properly.  For now, we'll just fix it not to use
693     #      the old Pg interface though
694     session = DBConn().session()
695     # Process suite
696     if Options["Suite"]:
697         suite_ids_list = []
698         for suitename in split_args(Options["Suite"]):
699             suite = get_suite(suitename, session=session)
700             if not suite or suite.suite_id is None:
701                 warn("suite '%s' not recognised." % (suite and suite.suite_name or suitename))
702             else:
703                 suite_ids_list.append(suite.suite_id)
704         if suite_ids_list:
705             con_suites = "AND su.id IN (%s)" % ", ".join([ str(i) for i in suite_ids_list ])
706         else:
707             fubar("No valid suite given.")
708     else:
709         con_suites = ""
710
711     # Process component
712     if Options["Component"]:
713         component_ids_list = []
714         for componentname in split_args(Options["Component"]):
715             component = get_component(componentname, session=session)
716             if component is None:
717                 warn("component '%s' not recognised." % (componentname))
718             else:
719                 component_ids_list.append(component.component_id)
720         if component_ids_list:
721             con_components = "AND c.id IN (%s)" % ", ".join([ str(i) for i in component_ids_list ])
722         else:
723             fubar("No valid component given.")
724     else:
725         con_components = ""
726
727     # Process architecture
728     con_architectures = ""
729     check_source = 0
730     if Options["Architecture"]:
731         arch_ids_list = []
732         for archname in split_args(Options["Architecture"]):
733             if archname == "source":
734                 check_source = 1
735             else:
736                 arch = get_architecture(archname, session=session)
737                 if arch is None:
738                     warn("architecture '%s' not recognised." % (archname))
739                 else:
740                     arch_ids_list.append(arch.arch_id)
741         if arch_ids_list:
742             con_architectures = "AND a.id IN (%s)" % ", ".join([ str(i) for i in arch_ids_list ])
743         else:
744             if not check_source:
745                 fubar("No valid architecture given.")
746     else:
747         check_source = 1
748
749     return (con_suites, con_architectures, con_components, check_source)
750
751 ################################################################################
752
753 def arch_compare_sw (a, b):
754     """
755     Function for use in sorting lists of architectures.
756
757     Sorts normally except that 'source' dominates all others.
758     """
759
760     if a == "source" and b == "source":
761         return 0
762     elif a == "source":
763         return -1
764     elif b == "source":
765         return 1
766
767     return cmp (a, b)
768
769 ################################################################################
770
771 def split_args (s, dwim=True):
772     """
773     Split command line arguments which can be separated by either commas
774     or whitespace.  If dwim is set, it will complain about string ending
775     in comma since this usually means someone did 'dak ls -a i386, m68k
776     foo' or something and the inevitable confusion resulting from 'm68k'
777     being treated as an argument is undesirable.
778     """
779
780     if s.find(",") == -1:
781         return s.split()
782     else:
783         if s[-1:] == "," and dwim:
784             fubar("split_args: found trailing comma, spurious space maybe?")
785         return s.split(",")
786
787 ################################################################################
788
789 def gpg_keyring_args(keyrings=None):
790     if not keyrings:
791         keyrings = get_active_keyring_paths()
792
793     return " ".join(["--keyring %s" % x for x in keyrings])
794
795 ################################################################################
796
797 def gpg_get_key_addresses(fingerprint):
798     """retreive email addresses from gpg key uids for a given fingerprint"""
799     addresses = key_uid_email_cache.get(fingerprint)
800     if addresses != None:
801         return addresses
802     addresses = list()
803     try:
804         with open(os.devnull, "wb") as devnull:
805             output = daklib.daksubprocess.check_output(
806                 ["gpg", "--no-default-keyring"] + gpg_keyring_args().split() +
807                 ["--with-colons", "--list-keys", fingerprint], stderr=devnull)
808     except subprocess.CalledProcessError:
809         pass
810     else:
811         for l in output.split('\n'):
812             parts = l.split(':')
813             if parts[0] not in ("uid", "pub"):
814                 continue
815             try:
816                 uid = parts[9]
817             except IndexError:
818                 continue
819             try:
820                 # Do not use unicode_escape, because it is locale-specific
821                 uid = codecs.decode(uid, "string_escape").decode("utf-8")
822             except UnicodeDecodeError:
823                 uid = uid.decode("latin1") # does not fail
824             m = re_parse_maintainer.match(uid)
825             if not m:
826                 continue
827             address = m.group(2)
828             address = address.encode("utf8") # dak still uses bytes
829             if address.endswith('@debian.org'):
830                 # prefer @debian.org addresses
831                 # TODO: maybe not hardcode the domain
832                 addresses.insert(0, address)
833             else:
834                 addresses.append(address)
835     key_uid_email_cache[fingerprint] = addresses
836     return addresses
837
838 ################################################################################
839
840 def get_logins_from_ldap(fingerprint='*'):
841     """retrieve login from LDAP linked to a given fingerprint"""
842
843     LDAPDn = Cnf['Import-LDAP-Fingerprints::LDAPDn']
844     LDAPServer = Cnf['Import-LDAP-Fingerprints::LDAPServer']
845     l = ldap.open(LDAPServer)
846     l.simple_bind_s('','')
847     Attrs = l.search_s(LDAPDn, ldap.SCOPE_ONELEVEL,
848                        '(keyfingerprint=%s)' % fingerprint,
849                        ['uid', 'keyfingerprint'])
850     login = {}
851     for elem in Attrs:
852         login[elem[1]['keyFingerPrint'][0]] = elem[1]['uid'][0]
853     return login
854
855 ################################################################################
856
857 def get_users_from_ldap():
858     """retrieve login and user names from LDAP"""
859
860     LDAPDn = Cnf['Import-LDAP-Fingerprints::LDAPDn']
861     LDAPServer = Cnf['Import-LDAP-Fingerprints::LDAPServer']
862     l = ldap.open(LDAPServer)
863     l.simple_bind_s('','')
864     Attrs = l.search_s(LDAPDn, ldap.SCOPE_ONELEVEL,
865                        '(uid=*)', ['uid', 'cn', 'mn', 'sn'])
866     users = {}
867     for elem in Attrs:
868         elem = elem[1]
869         name = []
870         for k in ('cn', 'mn', 'sn'):
871             try:
872                 if elem[k][0] != '-':
873                     name.append(elem[k][0])
874             except KeyError:
875                 pass
876         users[' '.join(name)] = elem['uid'][0]
877     return users
878
879 ################################################################################
880
881 def clean_symlink (src, dest, root):
882     """
883     Relativize an absolute symlink from 'src' -> 'dest' relative to 'root'.
884     Returns fixed 'src'
885     """
886     src = src.replace(root, '', 1)
887     dest = dest.replace(root, '', 1)
888     dest = os.path.dirname(dest)
889     new_src = '../' * len(dest.split('/'))
890     return new_src + src
891
892 ################################################################################
893
894 def temp_filename(directory=None, prefix="dak", suffix="", mode=None, group=None):
895     """
896     Return a secure and unique filename by pre-creating it.
897
898     @type directory: str
899     @param directory: If non-null it will be the directory the file is pre-created in.
900
901     @type prefix: str
902     @param prefix: The filename will be prefixed with this string
903
904     @type suffix: str
905     @param suffix: The filename will end with this string
906
907     @type mode: str
908     @param mode: If set the file will get chmodded to those permissions
909
910     @type group: str
911     @param group: If set the file will get chgrped to the specified group.
912
913     @rtype: list
914     @return: Returns a pair (fd, name)
915     """
916
917     (tfd, tfname) = tempfile.mkstemp(suffix, prefix, directory)
918     if mode:
919         os.chmod(tfname, mode)
920     if group:
921         gid = grp.getgrnam(group).gr_gid
922         os.chown(tfname, -1, gid)
923     return (tfd, tfname)
924
925 ################################################################################
926
927 def temp_dirname(parent=None, prefix="dak", suffix="", mode=None, group=None):
928     """
929     Return a secure and unique directory by pre-creating it.
930
931     @type parent: str
932     @param parent: If non-null it will be the directory the directory is pre-created in.
933
934     @type prefix: str
935     @param prefix: The filename will be prefixed with this string
936
937     @type suffix: str
938     @param suffix: The filename will end with this string
939
940     @type mode: str
941     @param mode: If set the file will get chmodded to those permissions
942
943     @type group: str
944     @param group: If set the file will get chgrped to the specified group.
945
946     @rtype: list
947     @return: Returns a pair (fd, name)
948
949     """
950
951     tfname = tempfile.mkdtemp(suffix, prefix, parent)
952     if mode:
953         os.chmod(tfname, mode)
954     if group:
955         gid = grp.getgrnam(group).gr_gid
956         os.chown(tfname, -1, gid)
957     return tfname
958
959 ################################################################################
960
961 def is_email_alias(email):
962     """ checks if the user part of the email is listed in the alias file """
963     global alias_cache
964     if alias_cache == None:
965         aliasfn = which_alias_file()
966         alias_cache = set()
967         if aliasfn:
968             for l in open(aliasfn):
969                 alias_cache.add(l.split(':')[0])
970     uid = email.split('@')[0]
971     return uid in alias_cache
972
973 ################################################################################
974
975 def get_changes_files(from_dir):
976     """
977     Takes a directory and lists all .changes files in it (as well as chdir'ing
978     to the directory; this is due to broken behaviour on the part of p-u/p-a
979     when you're not in the right place)
980
981     Returns a list of filenames
982     """
983     try:
984         # Much of the rest of p-u/p-a depends on being in the right place
985         os.chdir(from_dir)
986         changes_files = [x for x in os.listdir(from_dir) if x.endswith('.changes')]
987     except OSError as e:
988         fubar("Failed to read list from directory %s (%s)" % (from_dir, e))
989
990     return changes_files
991
992 ################################################################################
993
994 Cnf = config.Config().Cnf
995
996 ################################################################################
997
998 def parse_wnpp_bug_file(file = "/srv/ftp-master.debian.org/scripts/masterfiles/wnpp_rm"):
999     """
1000     Parses the wnpp bug list available at https://qa.debian.org/data/bts/wnpp_rm
1001     Well, actually it parsed a local copy, but let's document the source
1002     somewhere ;)
1003
1004     returns a dict associating source package name with a list of open wnpp
1005     bugs (Yes, there might be more than one)
1006     """
1007
1008     line = []
1009     try:
1010         f = open(file)
1011         lines = f.readlines()
1012     except IOError as e:
1013         print "Warning:  Couldn't open %s; don't know about WNPP bugs, so won't close any." % file
1014         lines = []
1015     wnpp = {}
1016
1017     for line in lines:
1018         splited_line = line.split(": ", 1)
1019         if len(splited_line) > 1:
1020             wnpp[splited_line[0]] = splited_line[1].split("|")
1021
1022     for source in wnpp.keys():
1023         bugs = []
1024         for wnpp_bug in wnpp[source]:
1025             bug_no = re.search("(\d)+", wnpp_bug).group()
1026             if bug_no:
1027                 bugs.append(bug_no)
1028         wnpp[source] = bugs
1029     return wnpp
1030
1031 ################################################################################
1032
1033 def get_packages_from_ftp(root, suite, component, architecture):
1034     """
1035     Returns an object containing apt_pkg-parseable data collected by
1036     aggregating Packages.gz files gathered for each architecture.
1037
1038     @type root: string
1039     @param root: path to ftp archive root directory
1040
1041     @type suite: string
1042     @param suite: suite to extract files from
1043
1044     @type component: string
1045     @param component: component to extract files from
1046
1047     @type architecture: string
1048     @param architecture: architecture to extract files from
1049
1050     @rtype: TagFile
1051     @return: apt_pkg class containing package data
1052     """
1053     filename = "%s/dists/%s/%s/binary-%s/Packages.gz" % (root, suite, component, architecture)
1054     (fd, temp_file) = temp_filename()
1055     (result, output) = commands.getstatusoutput("gunzip -c %s > %s" % (filename, temp_file))
1056     if (result != 0):
1057         fubar("Gunzip invocation failed!\n%s\n" % (output), result)
1058     filename = "%s/dists/%s/%s/debian-installer/binary-%s/Packages.gz" % (root, suite, component, architecture)
1059     if os.path.exists(filename):
1060         (result, output) = commands.getstatusoutput("gunzip -c %s >> %s" % (filename, temp_file))
1061         if (result != 0):
1062             fubar("Gunzip invocation failed!\n%s\n" % (output), result)
1063     packages = open_file(temp_file)
1064     Packages = apt_pkg.TagFile(packages)
1065     os.unlink(temp_file)
1066     return Packages
1067
1068 ################################################################################
1069
1070 def deb_extract_control(fh):
1071     """extract DEBIAN/control from a binary package"""
1072     return apt_inst.DebFile(fh).control.extractdata("control")
1073
1074 ################################################################################
1075
1076 def mail_addresses_for_upload(maintainer, changed_by, fingerprint):
1077     """mail addresses to contact for an upload
1078
1079     @type  maintainer: str
1080     @param maintainer: Maintainer field of the .changes file
1081
1082     @type  changed_by: str
1083     @param changed_by: Changed-By field of the .changes file
1084
1085     @type  fingerprint: str
1086     @param fingerprint: fingerprint of the key used to sign the upload
1087
1088     @rtype:  list of str
1089     @return: list of RFC 2047-encoded mail addresses to contact regarding
1090              this upload
1091     """
1092     addresses = [maintainer]
1093     if changed_by != maintainer:
1094         addresses.append(changed_by)
1095
1096     fpr_addresses = gpg_get_key_addresses(fingerprint)
1097     if len(fpr_addresses) > 0 and fix_maintainer(changed_by)[3] not in fpr_addresses and fix_maintainer(maintainer)[3] not in fpr_addresses:
1098         addresses.append(fpr_addresses[0])
1099
1100     encoded_addresses = [ fix_maintainer(e)[1] for e in addresses ]
1101     return encoded_addresses
1102
1103 ################################################################################
1104
1105 def call_editor(text="", suffix=".txt"):
1106     """run editor and return the result as a string
1107
1108     @type  text: str
1109     @param text: initial text
1110
1111     @type  suffix: str
1112     @param suffix: extension for temporary file
1113
1114     @rtype:  str
1115     @return: string with the edited text
1116     """
1117     editor = os.environ.get('VISUAL', os.environ.get('EDITOR', 'vi'))
1118     tmp = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
1119     try:
1120         print >>tmp, text,
1121         tmp.close()
1122         daklib.daksubprocess.check_call([editor, tmp.name])
1123         return open(tmp.name, 'r').read()
1124     finally:
1125         os.unlink(tmp.name)
1126
1127 ################################################################################
1128
1129 def check_reverse_depends(removals, suite, arches=None, session=None, cruft=False, quiet=False, include_arch_all=True):
1130     dbsuite = get_suite(suite, session)
1131     overridesuite = dbsuite
1132     if dbsuite.overridesuite is not None:
1133         overridesuite = get_suite(dbsuite.overridesuite, session)
1134     dep_problem = 0
1135     p2c = {}
1136     all_broken = defaultdict(lambda: defaultdict(set))
1137     if arches:
1138         all_arches = set(arches)
1139     else:
1140         all_arches = set(x.arch_string for x in get_suite_architectures(suite))
1141     all_arches -= set(["source", "all"])
1142     removal_set = set(removals)
1143     metakey_d = get_or_set_metadatakey("Depends", session)
1144     metakey_p = get_or_set_metadatakey("Provides", session)
1145     params = {
1146         'suite_id':     dbsuite.suite_id,
1147         'metakey_d_id': metakey_d.key_id,
1148         'metakey_p_id': metakey_p.key_id,
1149     }
1150     if include_arch_all:
1151         rdep_architectures = all_arches | set(['all'])
1152     else:
1153         rdep_architectures = all_arches
1154     for architecture in rdep_architectures:
1155         deps = {}
1156         sources = {}
1157         virtual_packages = {}
1158         params['arch_id'] = get_architecture(architecture, session).arch_id
1159
1160         statement = '''
1161             SELECT b.package, s.source, c.name as component,
1162                 (SELECT bmd.value FROM binaries_metadata bmd WHERE bmd.bin_id = b.id AND bmd.key_id = :metakey_d_id) AS depends,
1163                 (SELECT bmp.value FROM binaries_metadata bmp WHERE bmp.bin_id = b.id AND bmp.key_id = :metakey_p_id) AS provides
1164                 FROM binaries b
1165                 JOIN bin_associations ba ON b.id = ba.bin AND ba.suite = :suite_id
1166                 JOIN source s ON b.source = s.id
1167                 JOIN files_archive_map af ON b.file = af.file_id
1168                 JOIN component c ON af.component_id = c.id
1169                 WHERE b.architecture = :arch_id'''
1170         query = session.query('package', 'source', 'component', 'depends', 'provides'). \
1171             from_statement(statement).params(params)
1172         for package, source, component, depends, provides in query:
1173             sources[package] = source
1174             p2c[package] = component
1175             if depends is not None:
1176                 deps[package] = depends
1177             # Maintain a counter for each virtual package.  If a
1178             # Provides: exists, set the counter to 0 and count all
1179             # provides by a package not in the list for removal.
1180             # If the counter stays 0 at the end, we know that only
1181             # the to-be-removed packages provided this virtual
1182             # package.
1183             if provides is not None:
1184                 for virtual_pkg in provides.split(","):
1185                     virtual_pkg = virtual_pkg.strip()
1186                     if virtual_pkg == package: continue
1187                     if not virtual_packages.has_key(virtual_pkg):
1188                         virtual_packages[virtual_pkg] = 0
1189                     if package not in removals:
1190                         virtual_packages[virtual_pkg] += 1
1191
1192         # If a virtual package is only provided by the to-be-removed
1193         # packages, treat the virtual package as to-be-removed too.
1194         removal_set.update(virtual_pkg for virtual_pkg in virtual_packages if not virtual_packages[virtual_pkg])
1195
1196         # Check binary dependencies (Depends)
1197         for package in deps:
1198             if package in removals: continue
1199             try:
1200                 parsed_dep = apt_pkg.parse_depends(deps[package])
1201             except ValueError as e:
1202                 print "Error for package %s: %s" % (package, e)
1203                 parsed_dep = []
1204             for dep in parsed_dep:
1205                 # Check for partial breakage.  If a package has a ORed
1206                 # dependency, there is only a dependency problem if all
1207                 # packages in the ORed depends will be removed.
1208                 unsat = 0
1209                 for dep_package, _, _ in dep:
1210                     if dep_package in removals:
1211                         unsat += 1
1212                 if unsat == len(dep):
1213                     component = p2c[package]
1214                     source = sources[package]
1215                     if component != "main":
1216                         source = "%s/%s" % (source, component)
1217                     all_broken[source][package].add(architecture)
1218                     dep_problem = 1
1219
1220     if all_broken and not quiet:
1221         if cruft:
1222             print "  - broken Depends:"
1223         else:
1224             print "# Broken Depends:"
1225         for source, bindict in sorted(all_broken.items()):
1226             lines = []
1227             for binary, arches in sorted(bindict.items()):
1228                 if arches == all_arches or 'all' in arches:
1229                     lines.append(binary)
1230                 else:
1231                     lines.append('%s [%s]' % (binary, ' '.join(sorted(arches))))
1232             if cruft:
1233                 print '    %s: %s' % (source, lines[0])
1234             else:
1235                 print '%s: %s' % (source, lines[0])
1236             for line in lines[1:]:
1237                 if cruft:
1238                     print '    ' + ' ' * (len(source) + 2) + line
1239                 else:
1240                     print ' ' * (len(source) + 2) + line
1241         if not cruft:
1242             print
1243
1244     # Check source dependencies (Build-Depends and Build-Depends-Indep)
1245     all_broken = defaultdict(set)
1246     metakey_bd = get_or_set_metadatakey("Build-Depends", session)
1247     metakey_bdi = get_or_set_metadatakey("Build-Depends-Indep", session)
1248     if include_arch_all:
1249         metakey_ids = (metakey_bd.key_id, metakey_bdi.key_id)
1250     else:
1251         metakey_ids = (metakey_bd.key_id,)
1252
1253     params = {
1254         'suite_id':    dbsuite.suite_id,
1255         'metakey_ids': metakey_ids,
1256     }
1257     statement = '''
1258         SELECT s.source, string_agg(sm.value, ', ') as build_dep
1259            FROM source s
1260            JOIN source_metadata sm ON s.id = sm.src_id
1261            WHERE s.id in
1262                (SELECT src FROM newest_src_association
1263                    WHERE suite = :suite_id)
1264                AND sm.key_id in :metakey_ids
1265            GROUP BY s.id, s.source'''
1266     query = session.query('source', 'build_dep').from_statement(statement). \
1267         params(params)
1268     for source, build_dep in query:
1269         if source in removals: continue
1270         parsed_dep = []
1271         if build_dep is not None:
1272             # Remove [arch] information since we want to see breakage on all arches
1273             build_dep = re_build_dep_arch.sub("", build_dep)
1274             try:
1275                 parsed_dep = apt_pkg.parse_src_depends(build_dep)
1276             except ValueError as e:
1277                 print "Error for source %s: %s" % (source, e)
1278         for dep in parsed_dep:
1279             unsat = 0
1280             for dep_package, _, _ in dep:
1281                 if dep_package in removals:
1282                     unsat += 1
1283             if unsat == len(dep):
1284                 component, = session.query(Component.component_name) \
1285                     .join(Component.overrides) \
1286                     .filter(Override.suite == overridesuite) \
1287                     .filter(Override.package == re.sub('/(contrib|non-free)$', '', source)) \
1288                     .join(Override.overridetype).filter(OverrideType.overridetype == 'dsc') \
1289                     .first()
1290                 key = source
1291                 if component != "main":
1292                     key = "%s/%s" % (source, component)
1293                 all_broken[key].add(pp_deps(dep))
1294                 dep_problem = 1
1295
1296     if all_broken and not quiet:
1297         if cruft:
1298             print "  - broken Build-Depends:"
1299         else:
1300             print "# Broken Build-Depends:"
1301         for source, bdeps in sorted(all_broken.items()):
1302             bdeps = sorted(bdeps)
1303             if cruft:
1304                 print '    %s: %s' % (source, bdeps[0])
1305             else:
1306                 print '%s: %s' % (source, bdeps[0])
1307             for bdep in bdeps[1:]:
1308                 if cruft:
1309                     print '    ' + ' ' * (len(source) + 2) + bdep
1310                 else:
1311                     print ' ' * (len(source) + 2) + bdep
1312         if not cruft:
1313             print
1314
1315     return dep_problem
1316
1317 ################################################################################
1318
1319 def parse_built_using(control):
1320     """source packages referenced via Built-Using
1321
1322     @type  control: dict-like
1323     @param control: control file to take Built-Using field from
1324
1325     @rtype:  list of (str, str)
1326     @return: list of (source_name, source_version) pairs
1327     """
1328     built_using = control.get('Built-Using', None)
1329     if built_using is None:
1330         return []
1331
1332     bu = []
1333     for dep in apt_pkg.parse_depends(built_using):
1334         assert len(dep) == 1, 'Alternatives are not allowed in Built-Using field'
1335         source_name, source_version, comp = dep[0]
1336         assert comp == '=', 'Built-Using must contain strict dependencies'
1337         bu.append((source_name, source_version))
1338
1339     return bu
1340
1341 ################################################################################
1342
1343 def is_in_debug_section(control):
1344     """binary package is a debug package
1345
1346     @type  control: dict-like
1347     @param control: control file of binary package
1348
1349     @rtype Boolean
1350     @return: True if the binary package is a debug package
1351     """
1352     section = control['Section'].split('/', 1)[-1]
1353     auto_built_package = control.get("Auto-Built-Package")
1354     return section == "debug" and auto_built_package == "debug-symbols"