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