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