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