]> git.decadent.org.uk Git - dak.git/blob - utils.py
2004-06-24 James Troup <james@nocrew.org> * test/006/test.py (main): check brackete...
[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.68 2004-06-23 23:11:47 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:
331         email = maintainer;
332         name = "";
333     elif (maintainer[0] == "<" and maintainer[-1:] == ">"):
334         email = maintainer[1:-1];
335         name = "";
336     else:
337         m = re_parse_maintainer.match(maintainer);
338         if not m:
339             raise ParseMaintError, "Doesn't parse as a valid Maintainer field."
340         name = m.group(1);
341         email = m.group(2);
342
343     # Get an RFC2047 compliant version of the name
344     rfc2047_name = rfc2047_encode(name);
345
346     # Force the name to be UTF-8
347     name = force_to_utf8(name);
348
349     if name.find(',') != -1 or name.find('.') != -1:
350         rfc822_maint = "%s (%s)" % (email, name);
351         rfc2047_maint = "%s (%s)" % (email, rfc2047_name);
352     else:
353         rfc822_maint = "%s <%s>" % (name, email);
354         rfc2047_maint = "%s <%s>" % (rfc2047_name, email);
355
356     if email.find("@") == -1 and email.find("buildd_") != 0:
357         raise ParseMaintError, "No @ found in email address part."
358
359     return (rfc822_maint, rfc2047_maint, name, email);
360
361 ################################################################################
362
363 # sendmail wrapper, takes _either_ a message string or a file as arguments
364 def send_mail (message, filename=""):
365         # If we've been passed a string dump it into a temporary file
366         if message:
367             filename = tempfile.mktemp();
368             fd = os.open(filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0700);
369             os.write (fd, message);
370             os.close (fd);
371
372         # Invoke sendmail
373         (result, output) = commands.getstatusoutput("%s < %s" % (Cnf["Dinstall::SendmailCommand"], filename));
374         if (result != 0):
375             raise sendmail_failed_exc, output;
376
377         # Clean up any temporary files
378         if message:
379             os.unlink (filename);
380
381 ################################################################################
382
383 def poolify (source, component):
384     if component:
385         component += '/';
386     # FIXME: this is nasty
387     component = component.lower().replace("non-us/", "non-US/");
388     if source[:3] == "lib":
389         return component + source[:4] + '/' + source + '/'
390     else:
391         return component + source[:1] + '/' + source + '/'
392
393 ################################################################################
394
395 def move (src, dest, overwrite = 0, perms = 0664):
396     if os.path.exists(dest) and os.path.isdir(dest):
397         dest_dir = dest;
398     else:
399         dest_dir = os.path.dirname(dest);
400     if not os.path.exists(dest_dir):
401         umask = os.umask(00000);
402         os.makedirs(dest_dir, 02775);
403         os.umask(umask);
404     #print "Moving %s to %s..." % (src, dest);
405     if os.path.exists(dest) and os.path.isdir(dest):
406         dest += '/' + os.path.basename(src);
407     # Don't overwrite unless forced to
408     if os.path.exists(dest):
409         if not overwrite:
410             fubar("Can't move %s to %s - file already exists." % (src, dest));
411         else:
412             if not os.access(dest, os.W_OK):
413                 fubar("Can't move %s to %s - can't write to existing file." % (src, dest));
414     shutil.copy2(src, dest);
415     os.chmod(dest, perms);
416     os.unlink(src);
417
418 def copy (src, dest, overwrite = 0, perms = 0664):
419     if os.path.exists(dest) and os.path.isdir(dest):
420         dest_dir = dest;
421     else:
422         dest_dir = os.path.dirname(dest);
423     if not os.path.exists(dest_dir):
424         umask = os.umask(00000);
425         os.makedirs(dest_dir, 02775);
426         os.umask(umask);
427     #print "Copying %s to %s..." % (src, dest);
428     if os.path.exists(dest) and os.path.isdir(dest):
429         dest += '/' + os.path.basename(src);
430     # Don't overwrite unless forced to
431     if os.path.exists(dest):
432         if not overwrite:
433             raise file_exists_exc
434         else:
435             if not os.access(dest, os.W_OK):
436                 raise cant_overwrite_exc
437     shutil.copy2(src, dest);
438     os.chmod(dest, perms);
439
440 ################################################################################
441
442 def where_am_i ():
443     res = socket.gethostbyaddr(socket.gethostname());
444     database_hostname = Cnf.get("Config::" + res[0] + "::DatabaseHostname");
445     if database_hostname:
446         return database_hostname;
447     else:
448         return res[0];
449
450 def which_conf_file ():
451     res = socket.gethostbyaddr(socket.gethostname());
452     if Cnf.get("Config::" + res[0] + "::KatieConfig"):
453         return Cnf["Config::" + res[0] + "::KatieConfig"]
454     else:
455         return default_config;
456
457 def which_apt_conf_file ():
458     res = socket.gethostbyaddr(socket.gethostname());
459     if Cnf.get("Config::" + res[0] + "::AptConfig"):
460         return Cnf["Config::" + res[0] + "::AptConfig"]
461     else:
462         return default_apt_config;
463
464 ################################################################################
465
466 # Escape characters which have meaning to SQL's regex comparison operator ('~')
467 # (woefully incomplete)
468
469 def regex_safe (s):
470     s = s.replace('+', '\\\\+');
471     s = s.replace('.', '\\\\.');
472     return s
473
474 ################################################################################
475
476 # Perform a substition of template
477 def TemplateSubst(map, filename):
478     file = open_file(filename);
479     template = file.read();
480     for x in map.keys():
481         template = template.replace(x,map[x]);
482     file.close();
483     return template;
484
485 ################################################################################
486
487 def fubar(msg, exit_code=1):
488     sys.stderr.write("E: %s\n" % (msg));
489     sys.exit(exit_code);
490
491 def warn(msg):
492     sys.stderr.write("W: %s\n" % (msg));
493
494 ################################################################################
495
496 # Returns the user name with a laughable attempt at rfc822 conformancy
497 # (read: removing stray periods).
498 def whoami ():
499     return pwd.getpwuid(os.getuid())[4].split(',')[0].replace('.', '');
500
501 ################################################################################
502
503 def size_type (c):
504     t  = " B";
505     if c > 10240:
506         c = c / 1024;
507         t = " KB";
508     if c > 10240:
509         c = c / 1024;
510         t = " MB";
511     return ("%d%s" % (c, t))
512
513 ################################################################################
514
515 def cc_fix_changes (changes):
516     o = changes.get("architecture", "");
517     if o:
518         del changes["architecture"];
519     changes["architecture"] = {};
520     for j in o.split():
521         changes["architecture"][j] = 1;
522
523 # Sort by source name, source version, 'have source', and then by filename
524 def changes_compare (a, b):
525     try:
526         a_changes = parse_changes(a);
527     except:
528         return -1;
529
530     try:
531         b_changes = parse_changes(b);
532     except:
533         return 1;
534
535     cc_fix_changes (a_changes);
536     cc_fix_changes (b_changes);
537
538     # Sort by source name
539     a_source = a_changes.get("source");
540     b_source = b_changes.get("source");
541     q = cmp (a_source, b_source);
542     if q:
543         return q;
544
545     # Sort by source version
546     a_version = a_changes.get("version");
547     b_version = b_changes.get("version");
548     q = apt_pkg.VersionCompare(a_version, b_version);
549     if q:
550         return q;
551
552     # Sort by 'have source'
553     a_has_source = a_changes["architecture"].get("source");
554     b_has_source = b_changes["architecture"].get("source");
555     if a_has_source and not b_has_source:
556         return -1;
557     elif b_has_source and not a_has_source:
558         return 1;
559
560     # Fall back to sort by filename
561     return cmp(a, b);
562
563 ################################################################################
564
565 def find_next_free (dest, too_many=100):
566     extra = 0;
567     orig_dest = dest;
568     while os.path.exists(dest) and extra < too_many:
569         dest = orig_dest + '.' + repr(extra);
570         extra += 1;
571     if extra >= too_many:
572         raise tried_too_hard_exc;
573     return dest;
574
575 ################################################################################
576
577 def result_join (original, sep = '\t'):
578     list = [];
579     for i in xrange(len(original)):
580         if original[i] == None:
581             list.append("");
582         else:
583             list.append(original[i]);
584     return sep.join(list);
585
586 ################################################################################
587
588 def prefix_multi_line_string(str, prefix, include_blank_lines=0):
589     out = "";
590     for line in str.split('\n'):
591         line = line.strip();
592         if line or include_blank_lines:
593             out += "%s%s\n" % (prefix, line);
594     # Strip trailing new line
595     if out:
596         out = out[:-1];
597     return out;
598
599 ################################################################################
600
601 def validate_changes_file_arg(file, fatal=1):
602     error = None;
603
604     orig_filename = file
605     if file.endswith(".katie"):
606         file = file[:-6]+".changes";
607
608     if not file.endswith(".changes"):
609         error = "invalid file type; not a changes file";
610     else:
611         if not os.access(file,os.R_OK):
612             if os.path.exists(file):
613                 error = "permission denied";
614             else:
615                 error = "file not found";
616
617     if error:
618         if fatal:
619             fubar("%s: %s." % (orig_filename, error));
620         else:
621             warn("Skipping %s - %s" % (orig_filename, error));
622             return None;
623     else:
624         return file;
625
626 ################################################################################
627
628 def real_arch(arch):
629     return (arch != "source" and arch != "all");
630
631 ################################################################################
632
633 def join_with_commas_and(list):
634         if len(list) == 0: return "nothing";
635         if len(list) == 1: return list[0];
636         return ", ".join(list[:-1]) + " and " + list[-1];
637
638 ################################################################################
639
640 def get_conf():
641         return Cnf;
642
643 ################################################################################
644
645 # Handle -a, -c and -s arguments; returns them as SQL constraints
646 def parse_args(Options):
647     # Process suite
648     if Options["Suite"]:
649         suite_ids_list = [];
650         for suite in split_args(Options["Suite"]):
651             suite_id = db_access.get_suite_id(suite);
652             if suite_id == -1:
653                 warn("suite '%s' not recognised." % (suite));
654             else:
655                 suite_ids_list.append(suite_id);
656         if suite_ids_list:
657             con_suites = "AND su.id IN (%s)" % ", ".join(map(str, suite_ids_list));
658         else:
659             fubar("No valid suite given.");
660     else:
661         con_suites = "";
662
663     # Process component
664     if Options["Component"]:
665         component_ids_list = [];
666         for component in split_args(Options["Component"]):
667             component_id = db_access.get_component_id(component);
668             if component_id == -1:
669                 warn("component '%s' not recognised." % (component));
670             else:
671                 component_ids_list.append(component_id);
672         if component_ids_list:
673             con_components = "AND c.id IN (%s)" % ", ".join(map(str, component_ids_list));
674         else:
675             fubar("No valid component given.");
676     else:
677         con_components = "";
678
679     # Process architecture
680     con_architectures = "";
681     if Options["Architecture"]:
682         arch_ids_list = [];
683         check_source = 0;
684         for architecture in split_args(Options["Architecture"]):
685             if architecture == "source":
686                 check_source = 1;
687             else:
688                 architecture_id = db_access.get_architecture_id(architecture);
689                 if architecture_id == -1:
690                     warn("architecture '%s' not recognised." % (architecture));
691                 else:
692                     arch_ids_list.append(architecture_id);
693         if arch_ids_list:
694             con_architectures = "AND a.id IN (%s)" % ", ".join(map(str, arch_ids_list));
695         else:
696             if not check_source:
697                 fubar("No valid architecture given.");
698     else:
699         check_source = 1;
700
701     return (con_suites, con_architectures, con_components, check_source);
702
703 ################################################################################
704
705 # Inspired(tm) by Bryn Keller's print_exc_plus (See
706 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52215)
707
708 def print_exc():
709     tb = sys.exc_info()[2];
710     while tb.tb_next:
711         tb = tb.tb_next;
712     stack = [];
713     frame = tb.tb_frame;
714     while frame:
715         stack.append(frame);
716         frame = frame.f_back;
717     stack.reverse();
718     traceback.print_exc();
719     for frame in stack:
720         print "\nFrame %s in %s at line %s" % (frame.f_code.co_name,
721                                              frame.f_code.co_filename,
722                                              frame.f_lineno);
723         for key, value in frame.f_locals.items():
724             print "\t%20s = " % key,;
725             try:
726                 print value;
727             except:
728                 print "<unable to print>";
729
730 ################################################################################
731
732 def try_with_debug(function):
733     try:
734         function();
735     except SystemExit:
736         raise;
737     except:
738         print_exc();
739
740 ################################################################################
741
742 # Function for use in sorting lists of architectures.
743 # Sorts normally except that 'source' dominates all others.
744
745 def arch_compare_sw (a, b):
746     if a == "source" and b == "source":
747         return 0;
748     elif a == "source":
749         return -1;
750     elif b == "source":
751         return 1;
752
753     return cmp (a, b);
754
755 ################################################################################
756
757 # Split command line arguments which can be separated by either commas
758 # or whitespace.  If dwim is set, it will complain about string ending
759 # in comma since this usually means someone did 'madison -a i386, m68k
760 # foo' or something and the inevitable confusion resulting from 'm68k'
761 # being treated as an argument is undesirable.
762
763 def split_args (s, dwim=1):
764     if s.find(",") == -1:
765         return s.split();
766     else:
767         if s[-1:] == "," and dwim:
768             fubar("split_args: found trailing comma, spurious space maybe?");
769         return s.split(",");
770
771 ################################################################################
772
773 def Dict(**dict): return dict
774
775 ########################################
776
777 # Our very own version of commands.getouputstatus(), hacked to support
778 # gpgv's status fd.
779 def gpgv_get_status_output(cmd, status_read, status_write):
780     cmd = ['/bin/sh', '-c', cmd];
781     p2cread, p2cwrite = os.pipe();
782     c2pread, c2pwrite = os.pipe();
783     errout, errin = os.pipe();
784     pid = os.fork();
785     if pid == 0:
786         # Child
787         os.close(0);
788         os.close(1);
789         os.dup(p2cread);
790         os.dup(c2pwrite);
791         os.close(2);
792         os.dup(errin);
793         for i in range(3, 256):
794             if i != status_write:
795                 try:
796                     os.close(i);
797                 except:
798                     pass;
799         try:
800             os.execvp(cmd[0], cmd);
801         finally:
802             os._exit(1);
803
804     # Parent
805     os.close(p2cread)
806     os.dup2(c2pread, c2pwrite);
807     os.dup2(errout, errin);
808
809     output = status = "";
810     while 1:
811         i, o, e = select.select([c2pwrite, errin, status_read], [], []);
812         more_data = [];
813         for fd in i:
814             r = os.read(fd, 8196);
815             if len(r) > 0:
816                 more_data.append(fd);
817                 if fd == c2pwrite or fd == errin:
818                     output += r;
819                 elif fd == status_read:
820                     status += r;
821                 else:
822                     fubar("Unexpected file descriptor [%s] returned from select\n" % (fd));
823         if not more_data:
824             pid, exit_status = os.waitpid(pid, 0)
825             try:
826                 os.close(status_write);
827                 os.close(status_read);
828                 os.close(c2pread);
829                 os.close(c2pwrite);
830                 os.close(p2cwrite);
831                 os.close(errin);
832                 os.close(errout);
833             except:
834                 pass;
835             break;
836
837     return output, status, exit_status;
838
839 ############################################################
840
841
842 def check_signature (filename, reject):
843     """Check the signature of a file and return the fingerprint if the
844 signature is valid or 'None' if it's not.  The first argument is the
845 filename whose signature should be checked.  The second argument is a
846 reject function and is called when an error is found.  The reject()
847 function must allow for two arguments: the first is the error message,
848 the second is an optional prefix string.  It's possible for reject()
849 to be called more than once during an invocation of check_signature()."""
850
851     # Ensure the filename contains no shell meta-characters or other badness
852     if not re_taint_free.match(os.path.basename(filename)):
853         reject("!!WARNING!! tainted filename: '%s'." % (filename));
854         return 0;
855
856     # Invoke gpgv on the file
857     status_read, status_write = os.pipe();
858     cmd = "gpgv --status-fd %s --keyring %s --keyring %s %s" \
859           % (status_write, Cnf["Dinstall::PGPKeyring"], Cnf["Dinstall::GPGKeyring"], filename);
860     (output, status, exit_status) = gpgv_get_status_output(cmd, status_read, status_write);
861
862     # Process the status-fd output
863     keywords = {};
864     bad = internal_error = "";
865     for line in status.split('\n'):
866         line = line.strip();
867         if line == "":
868             continue;
869         split = line.split();
870         if len(split) < 2:
871             internal_error += "gpgv status line is malformed (< 2 atoms) ['%s'].\n" % (line);
872             continue;
873         (gnupg, keyword) = split[:2];
874         if gnupg != "[GNUPG:]":
875             internal_error += "gpgv status line is malformed (incorrect prefix '%s').\n" % (gnupg);
876             continue;
877         args = split[2:];
878         if keywords.has_key(keyword) and (keyword != "NODATA" and keyword != "SIGEXPIRED"):
879             internal_error += "found duplicate status token ('%s').\n" % (keyword);
880             continue;
881         else:
882             keywords[keyword] = args;
883
884     # If we failed to parse the status-fd output, let's just whine and bail now
885     if internal_error:
886         reject("internal error while performing signature check on %s." % (filename));
887         reject(internal_error, "");
888         reject("Please report the above errors to the Archive maintainers by replying to this mail.", "");
889         return None;
890
891     # Now check for obviously bad things in the processed output
892     if keywords.has_key("SIGEXPIRED"):
893         reject("The key used to sign %s has expired." % (filename));
894         bad = 1;
895     if keywords.has_key("KEYREVOKED"):
896         reject("The key used to sign %s has been revoked." % (filename));
897         bad = 1;
898     if keywords.has_key("BADSIG"):
899         reject("bad signature on %s." % (filename));
900         bad = 1;
901     if keywords.has_key("ERRSIG") and not keywords.has_key("NO_PUBKEY"):
902         reject("failed to check signature on %s." % (filename));
903         bad = 1;
904     if keywords.has_key("NO_PUBKEY"):
905         args = keywords["NO_PUBKEY"];
906         if len(args) >= 1:
907             key = args[0];
908         reject("The key (0x%s) used to sign %s wasn't found in the keyring(s)." % (key, filename));
909         bad = 1;
910     if keywords.has_key("BADARMOR"):
911         reject("ASCII armour of signature was corrupt in %s." % (filename));
912         bad = 1;
913     if keywords.has_key("NODATA"):
914         reject("no signature found in %s." % (filename));
915         bad = 1;
916
917     if bad:
918         return None;
919
920     # Next check gpgv exited with a zero return code
921     if exit_status:
922         reject("gpgv failed while checking %s." % (filename));
923         if status.strip():
924             reject(prefix_multi_line_string(status, " [GPG status-fd output:] "), "");
925         else:
926             reject(prefix_multi_line_string(output, " [GPG output:] "), "");
927         return None;
928
929     # Sanity check the good stuff we expect
930     if not keywords.has_key("VALIDSIG"):
931         reject("signature on %s does not appear to be valid [No VALIDSIG]." % (filename));
932         bad = 1;
933     else:
934         args = keywords["VALIDSIG"];
935         if len(args) < 1:
936             reject("internal error while checking signature on %s." % (filename));
937             bad = 1;
938         else:
939             fingerprint = args[0];
940     if not keywords.has_key("GOODSIG"):
941         reject("signature on %s does not appear to be valid [No GOODSIG]." % (filename));
942         bad = 1;
943     if not keywords.has_key("SIG_ID"):
944         reject("signature on %s does not appear to be valid [No SIG_ID]." % (filename));
945         bad = 1;
946
947     # Finally ensure there's not something we don't recognise
948     known_keywords = Dict(VALIDSIG="",SIG_ID="",GOODSIG="",BADSIG="",ERRSIG="",
949                           SIGEXPIRED="",KEYREVOKED="",NO_PUBKEY="",BADARMOR="",
950                           NODATA="");
951
952     for keyword in keywords.keys():
953         if not known_keywords.has_key(keyword):
954             reject("found unknown status token '%s' from gpgv with args '%r' in %s." % (keyword, keywords[keyword], filename));
955             bad = 1;
956
957     if bad:
958         return None;
959     else:
960         return fingerprint;
961
962 ################################################################################
963
964 # Inspired(tm) by http://www.zopelabs.com/cookbook/1022242603
965
966 def wrap(paragraph, max_length, prefix=""):
967     line = "";
968     s = "";
969     have_started = 0;
970     words = paragraph.split();
971
972     for word in words:
973         word_size = len(word);
974         if word_size > max_length:
975             if have_started:
976                 s += line + '\n' + prefix;
977             s += word + '\n' + prefix;
978         else:
979             if have_started:
980                 new_length = len(line) + word_size + 1;
981                 if new_length > max_length:
982                     s += line + '\n' + prefix;
983                     line = word;
984                 else:
985                     line += ' ' + word;
986             else:
987                 line = word;
988         have_started = 1;
989
990     if have_started:
991         s += line;
992
993     return s;
994
995 ################################################################################
996
997 # Relativize an absolute symlink from 'src' -> 'dest' relative to 'root'.
998 # Returns fixed 'src'
999 def clean_symlink (src, dest, root):
1000     src = src.replace(root, '', 1);
1001     dest = dest.replace(root, '', 1);
1002     dest = os.path.dirname(dest);
1003     new_src = '../' * len(dest.split('/'));
1004     return new_src + src;
1005
1006 ################################################################################
1007
1008 def temp_filename(directory=None, dotprefix=None, perms=0700):
1009     """Return a secure and unique filename by pre-creating it.
1010 If 'directory' is non-null, it will be the directory the file is pre-created in.
1011 If 'dotprefix' is non-null, the filename will be prefixed with a '.'."""
1012
1013     if directory:
1014         old_tempdir = tempfile.tempdir;
1015         tempfile.tempdir = directory;
1016
1017     filename = tempfile.mktemp();
1018
1019     if dotprefix:
1020         filename = "%s/.%s" % (os.path.dirname(filename), os.path.basename(filename));
1021     fd = os.open(filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, perms);
1022     os.close(fd);
1023
1024     if directory:
1025         tempfile.tempdir = old_tempdir;
1026
1027     return filename;
1028
1029 ################################################################################
1030
1031 apt_pkg.init();
1032
1033 Cnf = apt_pkg.newConfiguration();
1034 apt_pkg.ReadConfigFileISC(Cnf,default_config);
1035
1036 if which_conf_file() != default_config:
1037         apt_pkg.ReadConfigFileISC(Cnf,which_conf_file());
1038
1039 ################################################################################