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