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