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