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