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