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