]> git.decadent.org.uk Git - dak.git/blob - daklib/utils.py
Merge commit 'ftpmaster/master' into sqlalchemy
[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         checksum, size, checkfile = line.strip().split(' ')
398         if not files.has_key(checkfile):
399         # TODO: check for the file's entry in the original files dict, not
400         # the one modified by (auto)byhand and other weird stuff
401         #    rejmsg.append("%s: not present in files but in checksums-%s in %s" %
402         #        (file, hashname, where))
403             continue
404         if not files[checkfile]["size"] == size:
405             rejmsg.append("%s: size differs for files and checksums-%s entry "\
406                 "in %s" % (checkfile, hashname, where))
407             continue
408         files[checkfile][hash_key(hashname)] = checksum
409     for f in files.keys():
410         if not files[f].has_key(hash_key(hashname)):
411             rejmsg.append("%s: no entry in checksums-%s in %s" % (checkfile,
412                 hashname, where))
413     return rejmsg
414
415 ################################################################################
416
417 # Dropped support for 1.4 and ``buggy dchanges 3.4'' (?!) compared to di.pl
418
419 def build_file_list(changes, is_a_dsc=0, field="files", hashname="md5sum"):
420     files = {}
421
422     # Make sure we have a Files: field to parse...
423     if not changes.has_key(field):
424         raise NoFilesFieldError
425
426     # Make sure we recognise the format of the Files: field
427     format = re_verwithext.search(changes.get("format", "0.0"))
428     if not format:
429         raise UnknownFormatError, "%s" % (changes.get("format","0.0"))
430
431     format = format.groups()
432     if format[1] == None:
433         format = int(float(format[0])), 0, format[2]
434     else:
435         format = int(format[0]), int(format[1]), format[2]
436     if format[2] == None:
437         format = format[:2]
438
439     if is_a_dsc:
440         # format = (1,0) are the only formats we currently accept,
441         # format = (0,0) are missing format headers of which we still
442         # have some in the archive.
443         if format != (1,0) and format != (0,0):
444             raise UnknownFormatError, "%s" % (changes.get("format","0.0"))
445     else:
446         if (format < (1,5) or format > (1,8)):
447             raise UnknownFormatError, "%s" % (changes.get("format","0.0"))
448         if field != "files" and format < (1,8):
449             raise UnknownFormatError, "%s" % (changes.get("format","0.0"))
450
451     includes_section = (not is_a_dsc) and field == "files"
452
453     # Parse each entry/line:
454     for i in changes[field].split('\n'):
455         if not i:
456             break
457         s = i.split()
458         section = priority = ""
459         try:
460             if includes_section:
461                 (md5, size, section, priority, name) = s
462             else:
463                 (md5, size, name) = s
464         except ValueError:
465             raise ParseChangesError, i
466
467         if section == "":
468             section = "-"
469         if priority == "":
470             priority = "-"
471
472         (section, component) = extract_component_from_section(section)
473
474         files[name] = Dict(size=size, section=section,
475                            priority=priority, component=component)
476         files[name][hashname] = md5
477
478     return files
479
480 ################################################################################
481
482 def send_mail (message, filename=""):
483     """sendmail wrapper, takes _either_ a message string or a file as arguments"""
484
485     # If we've been passed a string dump it into a temporary file
486     if message:
487         (fd, filename) = tempfile.mkstemp()
488         os.write (fd, message)
489         os.close (fd)
490
491     if Cnf.has_key("Dinstall::MailWhiteList") and \
492            Cnf["Dinstall::MailWhiteList"] != "":
493         message_in = open_file(filename)
494         message_raw = modemail.message_from_file(message_in)
495         message_in.close();
496
497         whitelist = [];
498         whitelist_in = open_file(Cnf["Dinstall::MailWhiteList"])
499         try:
500             for line in whitelist_in:
501                 if not re_whitespace_comment.match(line):
502                     if re_re_mark.match(line):
503                         whitelist.append(re.compile(re_re_mark.sub("", line.strip(), 1)))
504                     else:
505                         whitelist.append(re.compile(re.escape(line.strip())))
506         finally:
507             whitelist_in.close()
508
509         # Fields to check.
510         fields = ["To", "Bcc", "Cc"]
511         for field in fields:
512             # Check each field
513             value = message_raw.get(field, None)
514             if value != None:
515                 match = [];
516                 for item in value.split(","):
517                     (rfc822_maint, rfc2047_maint, name, email) = fix_maintainer(item.strip())
518                     mail_whitelisted = 0
519                     for wr in whitelist:
520                         if wr.match(email):
521                             mail_whitelisted = 1
522                             break
523                     if not mail_whitelisted:
524                         print "Skipping %s since it's not in %s" % (item, Cnf["Dinstall::MailWhiteList"])
525                         continue
526                     match.append(item)
527
528                 # Doesn't have any mail in whitelist so remove the header
529                 if len(match) == 0:
530                     del message_raw[field]
531                 else:
532                     message_raw.replace_header(field, string.join(match, ", "))
533
534         # Change message fields in order if we don't have a To header
535         if not message_raw.has_key("To"):
536             fields.reverse()
537             for field in fields:
538                 if message_raw.has_key(field):
539                     message_raw[fields[-1]] = message_raw[field]
540                     del message_raw[field]
541                     break
542             else:
543                 # Clean up any temporary files
544                 # and return, as we removed all recipients.
545                 if message:
546                     os.unlink (filename);
547                 return;
548
549         fd = os.open(filename, os.O_RDWR|os.O_EXCL, 0700);
550         os.write (fd, message_raw.as_string(True));
551         os.close (fd);
552
553     # Invoke sendmail
554     (result, output) = commands.getstatusoutput("%s < %s" % (Cnf["Dinstall::SendmailCommand"], filename))
555     if (result != 0):
556         raise SendmailFailedError, output
557
558     # Clean up any temporary files
559     if message:
560         os.unlink (filename)
561
562 ################################################################################
563
564 def poolify (source, component):
565     if component:
566         component += '/'
567     if source[:3] == "lib":
568         return component + source[:4] + '/' + source + '/'
569     else:
570         return component + source[:1] + '/' + source + '/'
571
572 ################################################################################
573
574 def move (src, dest, overwrite = 0, perms = 0664):
575     if os.path.exists(dest) and os.path.isdir(dest):
576         dest_dir = dest
577     else:
578         dest_dir = os.path.dirname(dest)
579     if not os.path.exists(dest_dir):
580         umask = os.umask(00000)
581         os.makedirs(dest_dir, 02775)
582         os.umask(umask)
583     #print "Moving %s to %s..." % (src, dest)
584     if os.path.exists(dest) and os.path.isdir(dest):
585         dest += '/' + os.path.basename(src)
586     # Don't overwrite unless forced to
587     if os.path.exists(dest):
588         if not overwrite:
589             fubar("Can't move %s to %s - file already exists." % (src, dest))
590         else:
591             if not os.access(dest, os.W_OK):
592                 fubar("Can't move %s to %s - can't write to existing file." % (src, dest))
593     shutil.copy2(src, dest)
594     os.chmod(dest, perms)
595     os.unlink(src)
596
597 def copy (src, dest, overwrite = 0, perms = 0664):
598     if os.path.exists(dest) and os.path.isdir(dest):
599         dest_dir = dest
600     else:
601         dest_dir = os.path.dirname(dest)
602     if not os.path.exists(dest_dir):
603         umask = os.umask(00000)
604         os.makedirs(dest_dir, 02775)
605         os.umask(umask)
606     #print "Copying %s to %s..." % (src, dest)
607     if os.path.exists(dest) and os.path.isdir(dest):
608         dest += '/' + os.path.basename(src)
609     # Don't overwrite unless forced to
610     if os.path.exists(dest):
611         if not overwrite:
612             raise FileExistsError
613         else:
614             if not os.access(dest, os.W_OK):
615                 raise CantOverwriteError
616     shutil.copy2(src, dest)
617     os.chmod(dest, perms)
618
619 ################################################################################
620
621 def where_am_i ():
622     res = socket.gethostbyaddr(socket.gethostname())
623     database_hostname = Cnf.get("Config::" + res[0] + "::DatabaseHostname")
624     if database_hostname:
625         return database_hostname
626     else:
627         return res[0]
628
629 def which_conf_file ():
630     res = socket.gethostbyaddr(socket.gethostname())
631     # In case we allow local config files per user, try if one exists
632     if Cnf.FindB("Config::" + res[0] + "::AllowLocalConfig"):
633         homedir = os.getenv("HOME")
634         confpath = os.path.join(homedir, "/etc/dak.conf")
635         if os.path.exists(confpath):
636             apt_pkg.ReadConfigFileISC(Cnf,default_config)
637
638     # We are still in here, so there is no local config file or we do
639     # not allow local files. Do the normal stuff.
640     if Cnf.get("Config::" + res[0] + "::DakConfig"):
641         return Cnf["Config::" + res[0] + "::DakConfig"]
642     else:
643         return default_config
644
645 def which_apt_conf_file ():
646     res = socket.gethostbyaddr(socket.gethostname())
647     # In case we allow local config files per user, try if one exists
648     if Cnf.FindB("Config::" + res[0] + "::AllowLocalConfig"):
649         homedir = os.getenv("HOME")
650         confpath = os.path.join(homedir, "/etc/dak.conf")
651         if os.path.exists(confpath):
652             apt_pkg.ReadConfigFileISC(Cnf,default_config)
653
654     if Cnf.get("Config::" + res[0] + "::AptConfig"):
655         return Cnf["Config::" + res[0] + "::AptConfig"]
656     else:
657         return default_apt_config
658
659 def which_alias_file():
660     hostname = socket.gethostbyaddr(socket.gethostname())[0]
661     aliasfn = '/var/lib/misc/'+hostname+'/forward-alias'
662     if os.path.exists(aliasfn):
663         return aliasfn
664     else:
665         return None
666
667 ################################################################################
668
669 def TemplateSubst(map, filename):
670     """ Perform a substition of template """
671     templatefile = open_file(filename)
672     template = templatefile.read()
673     for x in map.keys():
674         template = template.replace(x,map[x])
675     templatefile.close()
676     return template
677
678 ################################################################################
679
680 def fubar(msg, exit_code=1):
681     sys.stderr.write("E: %s\n" % (msg))
682     sys.exit(exit_code)
683
684 def warn(msg):
685     sys.stderr.write("W: %s\n" % (msg))
686
687 ################################################################################
688
689 # Returns the user name with a laughable attempt at rfc822 conformancy
690 # (read: removing stray periods).
691 def whoami ():
692     return pwd.getpwuid(os.getuid())[4].split(',')[0].replace('.', '')
693
694 def getusername ():
695     return pwd.getpwuid(os.getuid())[0]
696
697 ################################################################################
698
699 def size_type (c):
700     t  = " B"
701     if c > 10240:
702         c = c / 1024
703         t = " KB"
704     if c > 10240:
705         c = c / 1024
706         t = " MB"
707     return ("%d%s" % (c, t))
708
709 ################################################################################
710
711 def cc_fix_changes (changes):
712     o = changes.get("architecture", "")
713     if o:
714         del changes["architecture"]
715     changes["architecture"] = {}
716     for j in o.split():
717         changes["architecture"][j] = 1
718
719 def changes_compare (a, b):
720     """ Sort by source name, source version, 'have source', and then by filename """
721     try:
722         a_changes = parse_changes(a)
723     except:
724         return -1
725
726     try:
727         b_changes = parse_changes(b)
728     except:
729         return 1
730
731     cc_fix_changes (a_changes)
732     cc_fix_changes (b_changes)
733
734     # Sort by source name
735     a_source = a_changes.get("source")
736     b_source = b_changes.get("source")
737     q = cmp (a_source, b_source)
738     if q:
739         return q
740
741     # Sort by source version
742     a_version = a_changes.get("version", "0")
743     b_version = b_changes.get("version", "0")
744     q = apt_pkg.VersionCompare(a_version, b_version)
745     if q:
746         return q
747
748     # Sort by 'have source'
749     a_has_source = a_changes["architecture"].get("source")
750     b_has_source = b_changes["architecture"].get("source")
751     if a_has_source and not b_has_source:
752         return -1
753     elif b_has_source and not a_has_source:
754         return 1
755
756     # Fall back to sort by filename
757     return cmp(a, b)
758
759 ################################################################################
760
761 def find_next_free (dest, too_many=100):
762     extra = 0
763     orig_dest = dest
764     while os.path.exists(dest) and extra < too_many:
765         dest = orig_dest + '.' + repr(extra)
766         extra += 1
767     if extra >= too_many:
768         raise NoFreeFilenameError
769     return dest
770
771 ################################################################################
772
773 def result_join (original, sep = '\t'):
774     resultlist = []
775     for i in xrange(len(original)):
776         if original[i] == None:
777             resultlist.append("")
778         else:
779             resultlist.append(original[i])
780     return sep.join(resultlist)
781
782 ################################################################################
783
784 def prefix_multi_line_string(str, prefix, include_blank_lines=0):
785     out = ""
786     for line in str.split('\n'):
787         line = line.strip()
788         if line or include_blank_lines:
789             out += "%s%s\n" % (prefix, line)
790     # Strip trailing new line
791     if out:
792         out = out[:-1]
793     return out
794
795 ################################################################################
796
797 def validate_changes_file_arg(filename, require_changes=1):
798     """
799     'filename' is either a .changes or .dak file.  If 'filename' is a
800     .dak file, it's changed to be the corresponding .changes file.  The
801     function then checks if the .changes file a) exists and b) is
802     readable and returns the .changes filename if so.  If there's a
803     problem, the next action depends on the option 'require_changes'
804     argument:
805
806       - If 'require_changes' == -1, errors are ignored and the .changes
807         filename is returned.
808       - If 'require_changes' == 0, a warning is given and 'None' is returned.
809       - If 'require_changes' == 1, a fatal error is raised.
810
811     """
812     error = None
813
814     orig_filename = filename
815     if filename.endswith(".dak"):
816         filename = filename[:-4]+".changes"
817
818     if not filename.endswith(".changes"):
819         error = "invalid file type; not a changes file"
820     else:
821         if not os.access(filename,os.R_OK):
822             if os.path.exists(filename):
823                 error = "permission denied"
824             else:
825                 error = "file not found"
826
827     if error:
828         if require_changes == 1:
829             fubar("%s: %s." % (orig_filename, error))
830         elif require_changes == 0:
831             warn("Skipping %s - %s" % (orig_filename, error))
832             return None
833         else: # We only care about the .dak file
834             return filename
835     else:
836         return filename
837
838 ################################################################################
839
840 def real_arch(arch):
841     return (arch != "source" and arch != "all")
842
843 ################################################################################
844
845 def join_with_commas_and(list):
846     if len(list) == 0: return "nothing"
847     if len(list) == 1: return list[0]
848     return ", ".join(list[:-1]) + " and " + list[-1]
849
850 ################################################################################
851
852 def pp_deps (deps):
853     pp_deps = []
854     for atom in deps:
855         (pkg, version, constraint) = atom
856         if constraint:
857             pp_dep = "%s (%s %s)" % (pkg, constraint, version)
858         else:
859             pp_dep = pkg
860         pp_deps.append(pp_dep)
861     return " |".join(pp_deps)
862
863 ################################################################################
864
865 def get_conf():
866     return Cnf
867
868 ################################################################################
869
870 def parse_args(Options):
871     """ Handle -a, -c and -s arguments; returns them as SQL constraints """
872     # XXX: This should go away and everything which calls it be converted
873     #      to use SQLA properly.  For now, we'll just fix it not to use
874     #      the old Pg interface though
875     session = DBConn().session()
876     # Process suite
877     if Options["Suite"]:
878         suite_ids_list = []
879         for suitename in split_args(Options["Suite"]):
880             suite = get_suite(suitename, session=session)
881             if suite.suite_id is None:
882                 warn("suite '%s' not recognised." % (suite.suite_name))
883             else:
884                 suite_ids_list.append(suite.suite_id)
885         if suite_ids_list:
886             con_suites = "AND su.id IN (%s)" % ", ".join([ str(i) for i in suite_ids_list ])
887         else:
888             fubar("No valid suite given.")
889     else:
890         con_suites = ""
891
892     # Process component
893     if Options["Component"]:
894         component_ids_list = []
895         for componentname in split_args(Options["Component"]):
896             component = get_component(componentname, session=session)
897             if component is None:
898                 warn("component '%s' not recognised." % (componentname))
899             else:
900                 component_ids_list.append(component.component_id)
901         if component_ids_list:
902             con_components = "AND c.id IN (%s)" % ", ".join([ str(i) for i in component_ids_list ])
903         else:
904             fubar("No valid component given.")
905     else:
906         con_components = ""
907
908     # Process architecture
909     con_architectures = ""
910     check_source = 0
911     if Options["Architecture"]:
912         arch_ids_list = []
913         for archname in split_args(Options["Architecture"]):
914             if archname == "source":
915                 check_source = 1
916             else:
917                 arch = get_architecture(archname, session=session)
918                 if arch is None:
919                     warn("architecture '%s' not recognised." % (archname))
920                 else:
921                     arch_ids_list.append(arch.arch_id)
922         if arch_ids_list:
923             con_architectures = "AND a.id IN (%s)" % ", ".join([ str(i) for i in arch_ids_list ])
924         else:
925             if not check_source:
926                 fubar("No valid architecture given.")
927     else:
928         check_source = 1
929
930     return (con_suites, con_architectures, con_components, check_source)
931
932 ################################################################################
933
934 # Inspired(tm) by Bryn Keller's print_exc_plus (See
935 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52215)
936
937 def print_exc():
938     tb = sys.exc_info()[2]
939     while tb.tb_next:
940         tb = tb.tb_next
941     stack = []
942     frame = tb.tb_frame
943     while frame:
944         stack.append(frame)
945         frame = frame.f_back
946     stack.reverse()
947     traceback.print_exc()
948     for frame in stack:
949         print "\nFrame %s in %s at line %s" % (frame.f_code.co_name,
950                                              frame.f_code.co_filename,
951                                              frame.f_lineno)
952         for key, value in frame.f_locals.items():
953             print "\t%20s = " % key,
954             try:
955                 print value
956             except:
957                 print "<unable to print>"
958
959 ################################################################################
960
961 def try_with_debug(function):
962     try:
963         function()
964     except SystemExit:
965         raise
966     except:
967         print_exc()
968
969 ################################################################################
970
971 def arch_compare_sw (a, b):
972     """
973     Function for use in sorting lists of architectures.
974
975     Sorts normally except that 'source' dominates all others.
976     """
977
978     if a == "source" and b == "source":
979         return 0
980     elif a == "source":
981         return -1
982     elif b == "source":
983         return 1
984
985     return cmp (a, b)
986
987 ################################################################################
988
989 def split_args (s, dwim=1):
990     """
991     Split command line arguments which can be separated by either commas
992     or whitespace.  If dwim is set, it will complain about string ending
993     in comma since this usually means someone did 'dak ls -a i386, m68k
994     foo' or something and the inevitable confusion resulting from 'm68k'
995     being treated as an argument is undesirable.
996     """
997
998     if s.find(",") == -1:
999         return s.split()
1000     else:
1001         if s[-1:] == "," and dwim:
1002             fubar("split_args: found trailing comma, spurious space maybe?")
1003         return s.split(",")
1004
1005 ################################################################################
1006
1007 def Dict(**dict): return dict
1008
1009 ########################################
1010
1011 def gpgv_get_status_output(cmd, status_read, status_write):
1012     """
1013     Our very own version of commands.getouputstatus(), hacked to support
1014     gpgv's status fd.
1015     """
1016
1017     cmd = ['/bin/sh', '-c', cmd]
1018     p2cread, p2cwrite = os.pipe()
1019     c2pread, c2pwrite = os.pipe()
1020     errout, errin = os.pipe()
1021     pid = os.fork()
1022     if pid == 0:
1023         # Child
1024         os.close(0)
1025         os.close(1)
1026         os.dup(p2cread)
1027         os.dup(c2pwrite)
1028         os.close(2)
1029         os.dup(errin)
1030         for i in range(3, 256):
1031             if i != status_write:
1032                 try:
1033                     os.close(i)
1034                 except:
1035                     pass
1036         try:
1037             os.execvp(cmd[0], cmd)
1038         finally:
1039             os._exit(1)
1040
1041     # Parent
1042     os.close(p2cread)
1043     os.dup2(c2pread, c2pwrite)
1044     os.dup2(errout, errin)
1045
1046     output = status = ""
1047     while 1:
1048         i, o, e = select.select([c2pwrite, errin, status_read], [], [])
1049         more_data = []
1050         for fd in i:
1051             r = os.read(fd, 8196)
1052             if len(r) > 0:
1053                 more_data.append(fd)
1054                 if fd == c2pwrite or fd == errin:
1055                     output += r
1056                 elif fd == status_read:
1057                     status += r
1058                 else:
1059                     fubar("Unexpected file descriptor [%s] returned from select\n" % (fd))
1060         if not more_data:
1061             pid, exit_status = os.waitpid(pid, 0)
1062             try:
1063                 os.close(status_write)
1064                 os.close(status_read)
1065                 os.close(c2pread)
1066                 os.close(c2pwrite)
1067                 os.close(p2cwrite)
1068                 os.close(errin)
1069                 os.close(errout)
1070             except:
1071                 pass
1072             break
1073
1074     return output, status, exit_status
1075
1076 ################################################################################
1077
1078 def process_gpgv_output(status):
1079     # Process the status-fd output
1080     keywords = {}
1081     internal_error = ""
1082     for line in status.split('\n'):
1083         line = line.strip()
1084         if line == "":
1085             continue
1086         split = line.split()
1087         if len(split) < 2:
1088             internal_error += "gpgv status line is malformed (< 2 atoms) ['%s'].\n" % (line)
1089             continue
1090         (gnupg, keyword) = split[:2]
1091         if gnupg != "[GNUPG:]":
1092             internal_error += "gpgv status line is malformed (incorrect prefix '%s').\n" % (gnupg)
1093             continue
1094         args = split[2:]
1095         if keywords.has_key(keyword) and keyword not in [ "NODATA", "SIGEXPIRED", "KEYEXPIRED" ]:
1096             internal_error += "found duplicate status token ('%s').\n" % (keyword)
1097             continue
1098         else:
1099             keywords[keyword] = args
1100
1101     return (keywords, internal_error)
1102
1103 ################################################################################
1104
1105 def retrieve_key (filename, keyserver=None, keyring=None):
1106     """
1107     Retrieve the key that signed 'filename' from 'keyserver' and
1108     add it to 'keyring'.  Returns nothing on success, or an error message
1109     on error.
1110     """
1111
1112     # Defaults for keyserver and keyring
1113     if not keyserver:
1114         keyserver = Cnf["Dinstall::KeyServer"]
1115     if not keyring:
1116         keyring = Cnf.ValueList("Dinstall::GPGKeyring")[0]
1117
1118     # Ensure the filename contains no shell meta-characters or other badness
1119     if not re_taint_free.match(filename):
1120         return "%s: tainted filename" % (filename)
1121
1122     # Invoke gpgv on the file
1123     status_read, status_write = os.pipe()
1124     cmd = "gpgv --status-fd %s --keyring /dev/null %s" % (status_write, filename)
1125     (_, status, _) = gpgv_get_status_output(cmd, status_read, status_write)
1126
1127     # Process the status-fd output
1128     (keywords, internal_error) = process_gpgv_output(status)
1129     if internal_error:
1130         return internal_error
1131
1132     if not keywords.has_key("NO_PUBKEY"):
1133         return "didn't find expected NO_PUBKEY in gpgv status-fd output"
1134
1135     fingerprint = keywords["NO_PUBKEY"][0]
1136     # XXX - gpg sucks.  You can't use --secret-keyring=/dev/null as
1137     # it'll try to create a lockfile in /dev.  A better solution might
1138     # be a tempfile or something.
1139     cmd = "gpg --no-default-keyring --secret-keyring=%s --no-options" \
1140           % (Cnf["Dinstall::SigningKeyring"])
1141     cmd += " --keyring %s --keyserver %s --recv-key %s" \
1142            % (keyring, keyserver, fingerprint)
1143     (result, output) = commands.getstatusoutput(cmd)
1144     if (result != 0):
1145         return "'%s' failed with exit code %s" % (cmd, result)
1146
1147     return ""
1148
1149 ################################################################################
1150
1151 def gpg_keyring_args(keyrings=None):
1152     if not keyrings:
1153         keyrings = Cnf.ValueList("Dinstall::GPGKeyring")
1154
1155     return " ".join(["--keyring %s" % x for x in keyrings])
1156
1157 ################################################################################
1158
1159 def check_signature (sig_filename, data_filename="", keyrings=None, autofetch=None):
1160     """
1161     Check the signature of a file and return the fingerprint if the
1162     signature is valid or 'None' if it's not.  The first argument is the
1163     filename whose signature should be checked.  The second argument is a
1164     reject function and is called when an error is found.  The reject()
1165     function must allow for two arguments: the first is the error message,
1166     the second is an optional prefix string.  It's possible for reject()
1167     to be called more than once during an invocation of check_signature().
1168     The third argument is optional and is the name of the files the
1169     detached signature applies to.  The fourth argument is optional and is
1170     a *list* of keyrings to use.  'autofetch' can either be None, True or
1171     False.  If None, the default behaviour specified in the config will be
1172     used.
1173     """
1174
1175     rejects = []
1176
1177     # Ensure the filename contains no shell meta-characters or other badness
1178     if not re_taint_free.match(sig_filename):
1179         rejects.append("!!WARNING!! tainted signature filename: '%s'." % (sig_filename))
1180         return (None, rejects)
1181
1182     if data_filename and not re_taint_free.match(data_filename):
1183         rejects.append("!!WARNING!! tainted data filename: '%s'." % (data_filename))
1184         return (None, rejects)
1185
1186     if not keyrings:
1187         keyrings = Cnf.ValueList("Dinstall::GPGKeyring")
1188
1189     # Autofetch the signing key if that's enabled
1190     if autofetch == None:
1191         autofetch = Cnf.get("Dinstall::KeyAutoFetch")
1192     if autofetch:
1193         error_msg = retrieve_key(sig_filename)
1194         if error_msg:
1195             rejects.append(error_msg)
1196             return (None, rejects)
1197
1198     # Build the command line
1199     status_read, status_write = os.pipe()
1200     cmd = "gpgv --status-fd %s %s %s %s" % (
1201         status_write, gpg_keyring_args(keyrings), sig_filename, data_filename)
1202
1203     # Invoke gpgv on the file
1204     (output, status, exit_status) = gpgv_get_status_output(cmd, status_read, status_write)
1205
1206     # Process the status-fd output
1207     (keywords, internal_error) = process_gpgv_output(status)
1208
1209     # If we failed to parse the status-fd output, let's just whine and bail now
1210     if internal_error:
1211         rejects.append("internal error while performing signature check on %s." % (sig_filename))
1212         rejects.append(internal_error, "")
1213         rejects.append("Please report the above errors to the Archive maintainers by replying to this mail.", "")
1214         return (None, rejects)
1215
1216     # Now check for obviously bad things in the processed output
1217     if keywords.has_key("KEYREVOKED"):
1218         rejects.append("The key used to sign %s has been revoked." % (sig_filename))
1219     if keywords.has_key("BADSIG"):
1220         rejects.append("bad signature on %s." % (sig_filename))
1221     if keywords.has_key("ERRSIG") and not keywords.has_key("NO_PUBKEY"):
1222         rejects.append("failed to check signature on %s." % (sig_filename))
1223     if keywords.has_key("NO_PUBKEY"):
1224         args = keywords["NO_PUBKEY"]
1225         if len(args) >= 1:
1226             key = args[0]
1227         rejects.append("The key (0x%s) used to sign %s wasn't found in the keyring(s)." % (key, sig_filename))
1228     if keywords.has_key("BADARMOR"):
1229         rejects.append("ASCII armour of signature was corrupt in %s." % (sig_filename))
1230     if keywords.has_key("NODATA"):
1231         rejects.append("no signature found in %s." % (sig_filename))
1232     if keywords.has_key("EXPKEYSIG"):
1233         args = keywords["EXPKEYSIG"]
1234         if len(args) >= 1:
1235             key = args[0]
1236         rejects.append("Signature made by expired key 0x%s" % (key))
1237     if keywords.has_key("KEYEXPIRED") and not keywords.has_key("GOODSIG"):
1238         args = keywords["KEYEXPIRED"]
1239         expiredate=""
1240         if len(args) >= 1:
1241             timestamp = args[0]
1242             if timestamp.count("T") == 0:
1243                 try:
1244                     expiredate = time.strftime("%Y-%m-%d", time.gmtime(float(timestamp)))
1245                 except ValueError:
1246                     expiredate = "unknown (%s)" % (timestamp)
1247             else:
1248                 expiredate = timestamp
1249         rejects.append("The key used to sign %s has expired on %s" % (sig_filename, expiredate))
1250
1251     if len(rejects) > 0:
1252         return (None, rejects)
1253
1254     # Next check gpgv exited with a zero return code
1255     if exit_status:
1256         rejects.append("gpgv failed while checking %s." % (sig_filename))
1257         if status.strip():
1258             rejects.append(prefix_multi_line_string(status, " [GPG status-fd output:] "), "")
1259         else:
1260             rejects.append(prefix_multi_line_string(output, " [GPG output:] "), "")
1261         return (None, rejects)
1262
1263     # Sanity check the good stuff we expect
1264     if not keywords.has_key("VALIDSIG"):
1265         rejects.append("signature on %s does not appear to be valid [No VALIDSIG]." % (sig_filename))
1266     else:
1267         args = keywords["VALIDSIG"]
1268         if len(args) < 1:
1269             rejects.append("internal error while checking signature on %s." % (sig_filename))
1270         else:
1271             fingerprint = args[0]
1272     if not keywords.has_key("GOODSIG"):
1273         rejects.append("signature on %s does not appear to be valid [No GOODSIG]." % (sig_filename))
1274     if not keywords.has_key("SIG_ID"):
1275         rejects.append("signature on %s does not appear to be valid [No SIG_ID]." % (sig_filename))
1276
1277     # Finally ensure there's not something we don't recognise
1278     known_keywords = Dict(VALIDSIG="",SIG_ID="",GOODSIG="",BADSIG="",ERRSIG="",
1279                           SIGEXPIRED="",KEYREVOKED="",NO_PUBKEY="",BADARMOR="",
1280                           NODATA="",NOTATION_DATA="",NOTATION_NAME="",KEYEXPIRED="")
1281
1282     for keyword in keywords.keys():
1283         if not known_keywords.has_key(keyword):
1284             rejects.append("found unknown status token '%s' from gpgv with args '%r' in %s." % (keyword, keywords[keyword], sig_filename))
1285
1286     if len(rejects) > 0:
1287         return (None, rejects)
1288     else:
1289         return (fingerprint, [])
1290
1291 ################################################################################
1292
1293 def gpg_get_key_addresses(fingerprint):
1294     """retreive email addresses from gpg key uids for a given fingerprint"""
1295     addresses = key_uid_email_cache.get(fingerprint)
1296     if addresses != None:
1297         return addresses
1298     addresses = set()
1299     cmd = "gpg --no-default-keyring %s --fingerprint %s" \
1300                 % (gpg_keyring_args(), fingerprint)
1301     (result, output) = commands.getstatusoutput(cmd)
1302     if result == 0:
1303         for l in output.split('\n'):
1304             m = re_gpg_uid.match(l)
1305             if m:
1306                 addresses.add(m.group(1))
1307     key_uid_email_cache[fingerprint] = addresses
1308     return addresses
1309
1310 ################################################################################
1311
1312 # Inspired(tm) by http://www.zopelabs.com/cookbook/1022242603
1313
1314 def wrap(paragraph, max_length, prefix=""):
1315     line = ""
1316     s = ""
1317     have_started = 0
1318     words = paragraph.split()
1319
1320     for word in words:
1321         word_size = len(word)
1322         if word_size > max_length:
1323             if have_started:
1324                 s += line + '\n' + prefix
1325             s += word + '\n' + prefix
1326         else:
1327             if have_started:
1328                 new_length = len(line) + word_size + 1
1329                 if new_length > max_length:
1330                     s += line + '\n' + prefix
1331                     line = word
1332                 else:
1333                     line += ' ' + word
1334             else:
1335                 line = word
1336         have_started = 1
1337
1338     if have_started:
1339         s += line
1340
1341     return s
1342
1343 ################################################################################
1344
1345 def clean_symlink (src, dest, root):
1346     """
1347     Relativize an absolute symlink from 'src' -> 'dest' relative to 'root'.
1348     Returns fixed 'src'
1349     """
1350     src = src.replace(root, '', 1)
1351     dest = dest.replace(root, '', 1)
1352     dest = os.path.dirname(dest)
1353     new_src = '../' * len(dest.split('/'))
1354     return new_src + src
1355
1356 ################################################################################
1357
1358 def temp_filename(directory=None, prefix="dak", suffix=""):
1359     """
1360     Return a secure and unique filename by pre-creating it.
1361     If 'directory' is non-null, it will be the directory the file is pre-created in.
1362     If 'prefix' is non-null, the filename will be prefixed with it, default is dak.
1363     If 'suffix' is non-null, the filename will end with it.
1364
1365     Returns a pair (fd, name).
1366     """
1367
1368     return tempfile.mkstemp(suffix, prefix, directory)
1369
1370 ################################################################################
1371
1372 def temp_dirname(parent=None, prefix="dak", suffix=""):
1373     """
1374     Return a secure and unique directory by pre-creating it.
1375     If 'parent' is non-null, it will be the directory the directory is pre-created in.
1376     If 'prefix' is non-null, the filename will be prefixed with it, default is dak.
1377     If 'suffix' is non-null, the filename will end with it.
1378
1379     Returns a pathname to the new directory
1380     """
1381
1382     return tempfile.mkdtemp(suffix, prefix, parent)
1383
1384 ################################################################################
1385
1386 def is_email_alias(email):
1387     """ checks if the user part of the email is listed in the alias file """
1388     global alias_cache
1389     if alias_cache == None:
1390         aliasfn = which_alias_file()
1391         alias_cache = set()
1392         if aliasfn:
1393             for l in open(aliasfn):
1394                 alias_cache.add(l.split(':')[0])
1395     uid = email.split('@')[0]
1396     return uid in alias_cache
1397
1398 ################################################################################
1399
1400 def get_changes_files(dir):
1401     """
1402     Takes a directory and lists all .changes files in it (as well as chdir'ing
1403     to the directory; this is due to broken behaviour on the part of p-u/p-a
1404     when you're not in the right place)
1405
1406     Returns a list of filenames
1407     """
1408     try:
1409         # Much of the rest of p-u/p-a depends on being in the right place
1410         os.chdir(dir)
1411         changes_files = [x for x in os.listdir(dir) if x.endswith('.changes')]
1412     except OSError, e:
1413         fubar("Failed to read list from directory %s (%s)" % (dir, e))
1414
1415     return changes_files
1416
1417 ################################################################################
1418
1419 apt_pkg.init()
1420
1421 Cnf = apt_pkg.newConfiguration()
1422 apt_pkg.ReadConfigFileISC(Cnf,default_config)
1423
1424 if which_conf_file() != default_config:
1425     apt_pkg.ReadConfigFileISC(Cnf,which_conf_file())
1426
1427 ###############################################################################