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