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