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