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