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