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