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