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