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