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