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