]> git.decadent.org.uk Git - dak.git/blob - daklib/utils.py
4e048d3ccccff4f90f827b47995d0d080684f5f1
[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["Dinstall::GPGKeyring"]
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 check_signature (sig_filename, reject, data_filename="", keyrings=None, autofetch=None):
943     """Check the signature of a file and return the fingerprint if the
944 signature is valid or 'None' if it's not.  The first argument is the
945 filename whose signature should be checked.  The second argument is a
946 reject function and is called when an error is found.  The reject()
947 function must allow for two arguments: the first is the error message,
948 the second is an optional prefix string.  It's possible for reject()
949 to be called more than once during an invocation of check_signature().
950 The third argument is optional and is the name of the files the
951 detached signature applies to.  The fourth argument is optional and is
952 a *list* of keyrings to use.  'autofetch' can either be None, True or
953 False.  If None, the default behaviour specified in the config will be
954 used."""
955
956     # Ensure the filename contains no shell meta-characters or other badness
957     if not re_taint_free.match(sig_filename):
958         reject("!!WARNING!! tainted signature filename: '%s'." % (sig_filename))
959         return None
960
961     if data_filename and not re_taint_free.match(data_filename):
962         reject("!!WARNING!! tainted data filename: '%s'." % (data_filename))
963         return None
964
965     if not keyrings:
966         keyrings = (Cnf["Dinstall::PGPKeyring"], Cnf["Dinstall::GPGKeyring"])
967
968     # Autofetch the signing key if that's enabled
969     if autofetch == None:
970         autofetch = Cnf.get("Dinstall::KeyAutoFetch")
971     if autofetch:
972         error_msg = retrieve_key(sig_filename)
973         if error_msg:
974             reject(error_msg)
975             return None
976
977     # Build the command line
978     status_read, status_write = os.pipe(); 
979     cmd = "gpgv --status-fd %s" % (status_write)
980     for keyring in keyrings:
981         cmd += " --keyring %s" % (keyring)
982     cmd += " %s %s" % (sig_filename, data_filename)
983     # Invoke gpgv on the file
984     (output, status, exit_status) = gpgv_get_status_output(cmd, status_read, status_write)
985
986     # Process the status-fd output
987     (keywords, internal_error) = process_gpgv_output(status)
988
989     # If we failed to parse the status-fd output, let's just whine and bail now
990     if internal_error:
991         reject("internal error while performing signature check on %s." % (sig_filename))
992         reject(internal_error, "")
993         reject("Please report the above errors to the Archive maintainers by replying to this mail.", "")
994         return None
995
996     bad = ""
997     # Now check for obviously bad things in the processed output
998     if keywords.has_key("SIGEXPIRED"):
999         reject("The key used to sign %s has expired." % (sig_filename))
1000         bad = 1
1001     if keywords.has_key("KEYREVOKED"):
1002         reject("The key used to sign %s has been revoked." % (sig_filename))
1003         bad = 1
1004     if keywords.has_key("BADSIG"):
1005         reject("bad signature on %s." % (sig_filename))
1006         bad = 1
1007     if keywords.has_key("ERRSIG") and not keywords.has_key("NO_PUBKEY"):
1008         reject("failed to check signature on %s." % (sig_filename))
1009         bad = 1
1010     if keywords.has_key("NO_PUBKEY"):
1011         args = keywords["NO_PUBKEY"]
1012         if len(args) >= 1:
1013             key = args[0]
1014         reject("The key (0x%s) used to sign %s wasn't found in the keyring(s)." % (key, sig_filename))
1015         bad = 1
1016     if keywords.has_key("BADARMOR"):
1017         reject("ASCII armour of signature was corrupt in %s." % (sig_filename))
1018         bad = 1
1019     if keywords.has_key("NODATA"):
1020         reject("no signature found in %s." % (sig_filename))
1021         bad = 1
1022
1023     if bad:
1024         return None
1025
1026     # Next check gpgv exited with a zero return code
1027     if exit_status:
1028         reject("gpgv failed while checking %s." % (sig_filename))
1029         if status.strip():
1030             reject(prefix_multi_line_string(status, " [GPG status-fd output:] "), "")
1031         else:
1032             reject(prefix_multi_line_string(output, " [GPG output:] "), "")
1033         return None
1034
1035     # Sanity check the good stuff we expect
1036     if not keywords.has_key("VALIDSIG"):
1037         reject("signature on %s does not appear to be valid [No VALIDSIG]." % (sig_filename))
1038         bad = 1
1039     else:
1040         args = keywords["VALIDSIG"]
1041         if len(args) < 1:
1042             reject("internal error while checking signature on %s." % (sig_filename))
1043             bad = 1
1044         else:
1045             fingerprint = args[0]
1046     if not keywords.has_key("GOODSIG"):
1047         reject("signature on %s does not appear to be valid [No GOODSIG]." % (sig_filename))
1048         bad = 1
1049     if not keywords.has_key("SIG_ID"):
1050         reject("signature on %s does not appear to be valid [No SIG_ID]." % (sig_filename))
1051         bad = 1
1052
1053     # Finally ensure there's not something we don't recognise
1054     known_keywords = Dict(VALIDSIG="",SIG_ID="",GOODSIG="",BADSIG="",ERRSIG="",
1055                           SIGEXPIRED="",KEYREVOKED="",NO_PUBKEY="",BADARMOR="",
1056                           NODATA="")
1057
1058     for keyword in keywords.keys():
1059         if not known_keywords.has_key(keyword):
1060             reject("found unknown status token '%s' from gpgv with args '%r' in %s." % (keyword, keywords[keyword], sig_filename))
1061             bad = 1
1062
1063     if bad:
1064         return None
1065     else:
1066         return fingerprint
1067
1068 ################################################################################
1069
1070 # Inspired(tm) by http://www.zopelabs.com/cookbook/1022242603
1071
1072 def wrap(paragraph, max_length, prefix=""):
1073     line = ""
1074     s = ""
1075     have_started = 0
1076     words = paragraph.split()
1077
1078     for word in words:
1079         word_size = len(word)
1080         if word_size > max_length:
1081             if have_started:
1082                 s += line + '\n' + prefix
1083             s += word + '\n' + prefix
1084         else:
1085             if have_started:
1086                 new_length = len(line) + word_size + 1
1087                 if new_length > max_length:
1088                     s += line + '\n' + prefix
1089                     line = word
1090                 else:
1091                     line += ' ' + word
1092             else:
1093                 line = word
1094         have_started = 1
1095
1096     if have_started:
1097         s += line
1098
1099     return s
1100
1101 ################################################################################
1102
1103 # Relativize an absolute symlink from 'src' -> 'dest' relative to 'root'.
1104 # Returns fixed 'src'
1105 def clean_symlink (src, dest, root):
1106     src = src.replace(root, '', 1)
1107     dest = dest.replace(root, '', 1)
1108     dest = os.path.dirname(dest)
1109     new_src = '../' * len(dest.split('/'))
1110     return new_src + src
1111
1112 ################################################################################
1113
1114 def temp_filename(directory=None, dotprefix=None, perms=0700):
1115     """Return a secure and unique filename by pre-creating it.
1116 If 'directory' is non-null, it will be the directory the file is pre-created in.
1117 If 'dotprefix' is non-null, the filename will be prefixed with a '.'."""
1118
1119     if directory:
1120         old_tempdir = tempfile.tempdir
1121         tempfile.tempdir = directory
1122
1123     filename = tempfile.mktemp()
1124
1125     if dotprefix:
1126         filename = "%s/.%s" % (os.path.dirname(filename), os.path.basename(filename))
1127     fd = os.open(filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, perms)
1128     os.close(fd)
1129
1130     if directory:
1131         tempfile.tempdir = old_tempdir
1132
1133     return filename
1134
1135 ################################################################################
1136
1137 apt_pkg.init()
1138
1139 Cnf = apt_pkg.newConfiguration()
1140 apt_pkg.ReadConfigFileISC(Cnf,default_config)
1141
1142 if which_conf_file() != default_config:
1143         apt_pkg.ReadConfigFileISC(Cnf,which_conf_file())
1144
1145 ################################################################################