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