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