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