4 # Copyright (C) 2000, 2001, 2002, 2003, 2004, 2005, 2006 James Troup <james@nocrew.org>
6 ################################################################################
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.
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.
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
22 ################################################################################
24 import codecs, commands, email.Header, os, pwd, re, select, socket, shutil, \
25 sys, tempfile, traceback
29 ################################################################################
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)$")
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]+$")
43 re_parse_maintainer = re.compile(r"^\s*(\S.*\S)\s*\<([^\>]+)\>")
45 re_srchasver = re.compile(r"^(\S+)\s+\((\S+)\)$")
46 re_verwithext = re.compile(r"^(\d+)(?:\.(\d+))(?:\s+\((\S+)\))?$")
48 changes_parse_error_exc = "Can't parse line in .changes file"
49 invalid_dsc_format_exc = "Invalid .dsc file"
50 nk_format_exc = "Unknown Format: in .changes file"
51 no_files_exc = "No Files: field in .dsc or .changes file."
52 cant_open_exc = "Can't open file"
53 unknown_hostname_exc = "Unknown hostname"
54 cant_overwrite_exc = "Permission denied; can't overwrite existent file."
55 file_exists_exc = "Destination file exists"
56 sendmail_failed_exc = "Sendmail invocation failed"
57 tried_too_hard_exc = "Tried too hard to find a free filename."
59 default_config = "/etc/dak/dak.conf"
60 default_apt_config = "/etc/dak/apt.conf"
62 ################################################################################
64 class Error(Exception):
65 """Base class for exceptions in this module."""
68 class ParseMaintError(Error):
69 """Exception raised for errors in parsing a maintainer field.
72 message -- explanation of the error
75 def __init__(self, message):
77 self.message = message
79 ################################################################################
81 def open_file(filename, mode='r'):
83 f = open(filename, mode)
85 raise cant_open_exc, filename
88 ################################################################################
90 def our_raw_input(prompt=""):
92 sys.stdout.write(prompt)
98 sys.stderr.write("\nUser interrupt (^D).\n")
101 ################################################################################
103 def extract_component_from_section(section):
106 if section.find('/') != -1:
107 component = section.split('/')[0]
109 # Expand default component
111 if Cnf.has_key("Component::%s" % section):
116 return (section, component)
118 ################################################################################
120 def parse_changes(filename, signing_rules=0):
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
125 signing_rules is an optional argument:
127 o If signing_rules == -1, no signature is required.
128 o If signing_rules == 0 (the default), a signature is required.
129 o If signing_rules == 1, it turns on the same strict format checking
132 The rules for (signing_rules == 1)-mode are:
134 o The PGP header consists of "-----BEGIN PGP SIGNED MESSAGE-----"
135 followed by any PGP header data and must end with a blank line.
137 o The data section must end with a blank line and must be followed by
138 "-----BEGIN PGP SIGNATURE-----".
144 changes_in = open_file(filename)
145 lines = changes_in.readlines()
148 raise changes_parse_error_exc, "[Empty changes file]"
150 # Reindex by line number so we can easily verify the format of
156 indexed_lines[index] = line[:-1]
160 num_of_lines = len(indexed_lines.keys())
163 while index < num_of_lines:
165 line = indexed_lines[index]
167 if signing_rules == 1:
169 if index > num_of_lines:
170 raise invalid_dsc_format_exc, index
171 line = indexed_lines[index]
172 if not line.startswith("-----BEGIN PGP SIGNATURE"):
173 raise invalid_dsc_format_exc, index
178 if line.startswith("-----BEGIN PGP SIGNATURE"):
180 if line.startswith("-----BEGIN PGP SIGNED MESSAGE"):
182 if signing_rules == 1:
183 while index < num_of_lines and line != "":
185 line = indexed_lines[index]
187 # If we're not inside the signed data, don't process anything
188 if signing_rules >= 0 and not inside_signature:
190 slf = re_single_line_field.match(line)
192 field = slf.groups()[0].lower()
193 changes[field] = slf.groups()[1]
197 changes[field] += '\n'
199 mlf = re_multi_line_field.match(line)
202 raise changes_parse_error_exc, "'%s'\n [Multi-line field continuing on from nothing?]" % (line)
203 if first == 1 and changes[field] != "":
204 changes[field] += '\n'
206 changes[field] += mlf.groups()[0] + '\n'
210 if signing_rules == 1 and inside_signature:
211 raise invalid_dsc_format_exc, index
214 changes["filecontents"] = "".join(lines)
216 if changes.has_key("source"):
217 # Strip the source version in brackets from the source field,
218 # put it in the "source-version" field instead.
219 srcver = re_srchasver.search(changes["source"])
221 changes["source"] = srcver.group(1)
222 changes["source-version"] = srcver.group(2)
225 raise changes_parse_error_exc, error
229 ################################################################################
231 # Dropped support for 1.4 and ``buggy dchanges 3.4'' (?!) compared to di.pl
233 def build_file_list(changes, is_a_dsc=0, field="files", hashname="md5sum"):
236 # Make sure we have a Files: field to parse...
237 if not changes.has_key(field):
240 # Make sure we recognise the format of the Files: field
241 format = re_verwithext.search(changes.get("format", "0.0"))
243 raise nk_format_exc, "%s" % (changes.get("format","0.0"))
245 format = format.groups()
246 if format[1] == None:
247 format = int(float(format[0])), 0, format[2]
249 format = int(format[0]), int(format[1]), format[2]
250 if format[2] == None:
255 raise nk_format_exc, "%s" % (changes.get("format","0.0"))
257 if (format < (1,5) or format > (1,8)):
258 raise nk_format_exc, "%s" % (changes.get("format","0.0"))
259 if field != "files" and format < (1,8):
260 raise nk_format_exc, "%s" % (changes.get("format","0.0"))
262 includes_section = (not is_a_dsc) and field == "files"
264 # Parse each entry/line:
265 for i in changes[field].split('\n'):
269 section = priority = ""
272 (md5, size, section, priority, name) = s
274 (md5, size, name) = s
276 raise changes_parse_error_exc, i
283 (section, component) = extract_component_from_section(section)
285 files[name] = Dict(size=size, section=section,
286 priority=priority, component=component)
287 files[name][hashname] = md5
291 ################################################################################
293 def force_to_utf8(s):
294 """Forces a string to UTF-8. If the string isn't already UTF-8,
295 it's assumed to be ISO-8859-1."""
300 latin1_s = unicode(s,'iso8859-1')
301 return latin1_s.encode('utf-8')
303 def rfc2047_encode(s):
304 """Encodes a (header) string per RFC2047 if necessary. If the
305 string is neither ASCII nor UTF-8, it's assumed to be ISO-8859-1."""
307 codecs.lookup('ascii')[1](s)
312 codecs.lookup('utf-8')[1](s)
313 h = email.Header.Header(s, 'utf-8', 998)
316 h = email.Header.Header(s, 'iso-8859-1', 998)
319 ################################################################################
321 # <Culus> 'The standard sucks, but my tool is supposed to interoperate
322 # with it. I know - I'll fix the suckage and make things
325 def fix_maintainer (maintainer):
326 """Parses a Maintainer or Changed-By field and returns:
327 (1) an RFC822 compatible version,
328 (2) an RFC2047 compatible version,
332 The name is forced to UTF-8 for both (1) and (3). If the name field
333 contains '.' or ',' (as allowed by Debian policy), (1) and (2) are
334 switched to 'email (name)' format."""
335 maintainer = maintainer.strip()
337 return ('', '', '', '')
339 if maintainer.find("<") == -1:
342 elif (maintainer[0] == "<" and maintainer[-1:] == ">"):
343 email = maintainer[1:-1]
346 m = re_parse_maintainer.match(maintainer)
348 raise ParseMaintError, "Doesn't parse as a valid Maintainer field."
352 # Get an RFC2047 compliant version of the name
353 rfc2047_name = rfc2047_encode(name)
355 # Force the name to be UTF-8
356 name = force_to_utf8(name)
358 if name.find(',') != -1 or name.find('.') != -1:
359 rfc822_maint = "%s (%s)" % (email, name)
360 rfc2047_maint = "%s (%s)" % (email, rfc2047_name)
362 rfc822_maint = "%s <%s>" % (name, email)
363 rfc2047_maint = "%s <%s>" % (rfc2047_name, email)
365 if email.find("@") == -1 and email.find("buildd_") != 0:
366 raise ParseMaintError, "No @ found in email address part."
368 return (rfc822_maint, rfc2047_maint, name, email)
370 ################################################################################
372 # sendmail wrapper, takes _either_ a message string or a file as arguments
373 def send_mail (message, filename=""):
374 # If we've been passed a string dump it into a temporary file
376 filename = tempfile.mktemp()
377 fd = os.open(filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0700)
378 os.write (fd, message)
382 (result, output) = commands.getstatusoutput("%s < %s" % (Cnf["Dinstall::SendmailCommand"], filename))
384 raise sendmail_failed_exc, output
386 # Clean up any temporary files
390 ################################################################################
392 def poolify (source, component):
395 if source[:3] == "lib":
396 return component + source[:4] + '/' + source + '/'
398 return component + source[:1] + '/' + source + '/'
400 ################################################################################
402 def move (src, dest, overwrite = 0, perms = 0664):
403 if os.path.exists(dest) and os.path.isdir(dest):
406 dest_dir = os.path.dirname(dest)
407 if not os.path.exists(dest_dir):
408 umask = os.umask(00000)
409 os.makedirs(dest_dir, 02775)
411 #print "Moving %s to %s..." % (src, dest)
412 if os.path.exists(dest) and os.path.isdir(dest):
413 dest += '/' + os.path.basename(src)
414 # Don't overwrite unless forced to
415 if os.path.exists(dest):
417 fubar("Can't move %s to %s - file already exists." % (src, dest))
419 if not os.access(dest, os.W_OK):
420 fubar("Can't move %s to %s - can't write to existing file." % (src, dest))
421 shutil.copy2(src, dest)
422 os.chmod(dest, perms)
425 def copy (src, dest, overwrite = 0, perms = 0664):
426 if os.path.exists(dest) and os.path.isdir(dest):
429 dest_dir = os.path.dirname(dest)
430 if not os.path.exists(dest_dir):
431 umask = os.umask(00000)
432 os.makedirs(dest_dir, 02775)
434 #print "Copying %s to %s..." % (src, dest)
435 if os.path.exists(dest) and os.path.isdir(dest):
436 dest += '/' + os.path.basename(src)
437 # Don't overwrite unless forced to
438 if os.path.exists(dest):
440 raise file_exists_exc
442 if not os.access(dest, os.W_OK):
443 raise cant_overwrite_exc
444 shutil.copy2(src, dest)
445 os.chmod(dest, perms)
447 ################################################################################
450 res = socket.gethostbyaddr(socket.gethostname())
451 database_hostname = Cnf.get("Config::" + res[0] + "::DatabaseHostname")
452 if database_hostname:
453 return database_hostname
457 def which_conf_file ():
458 res = socket.gethostbyaddr(socket.gethostname())
459 if Cnf.get("Config::" + res[0] + "::DakConfig"):
460 return Cnf["Config::" + res[0] + "::DakConfig"]
462 return default_config
464 def which_apt_conf_file ():
465 res = socket.gethostbyaddr(socket.gethostname())
466 if Cnf.get("Config::" + res[0] + "::AptConfig"):
467 return Cnf["Config::" + res[0] + "::AptConfig"]
469 return default_apt_config
471 def which_alias_file():
472 hostname = socket.gethostbyaddr(socket.gethostname())[0]
473 aliasfn = '/var/lib/misc/'+hostname+'/forward-alias'
474 if os.path.exists(aliasfn):
479 ################################################################################
481 # Escape characters which have meaning to SQL's regex comparison operator ('~')
482 # (woefully incomplete)
485 s = s.replace('+', '\\\\+')
486 s = s.replace('.', '\\\\.')
489 ################################################################################
491 # Perform a substition of template
492 def TemplateSubst(map, filename):
493 file = open_file(filename)
494 template = file.read()
496 template = template.replace(x,map[x])
500 ################################################################################
502 def fubar(msg, exit_code=1):
503 sys.stderr.write("E: %s\n" % (msg))
507 sys.stderr.write("W: %s\n" % (msg))
509 ################################################################################
511 # Returns the user name with a laughable attempt at rfc822 conformancy
512 # (read: removing stray periods).
514 return pwd.getpwuid(os.getuid())[4].split(',')[0].replace('.', '')
516 ################################################################################
526 return ("%d%s" % (c, t))
528 ################################################################################
530 def cc_fix_changes (changes):
531 o = changes.get("architecture", "")
533 del changes["architecture"]
534 changes["architecture"] = {}
536 changes["architecture"][j] = 1
538 # Sort by source name, source version, 'have source', and then by filename
539 def changes_compare (a, b):
541 a_changes = parse_changes(a)
546 b_changes = parse_changes(b)
550 cc_fix_changes (a_changes)
551 cc_fix_changes (b_changes)
553 # Sort by source name
554 a_source = a_changes.get("source")
555 b_source = b_changes.get("source")
556 q = cmp (a_source, b_source)
560 # Sort by source version
561 a_version = a_changes.get("version", "0")
562 b_version = b_changes.get("version", "0")
563 q = apt_pkg.VersionCompare(a_version, b_version)
567 # Sort by 'have source'
568 a_has_source = a_changes["architecture"].get("source")
569 b_has_source = b_changes["architecture"].get("source")
570 if a_has_source and not b_has_source:
572 elif b_has_source and not a_has_source:
575 # Fall back to sort by filename
578 ################################################################################
580 def find_next_free (dest, too_many=100):
583 while os.path.exists(dest) and extra < too_many:
584 dest = orig_dest + '.' + repr(extra)
586 if extra >= too_many:
587 raise tried_too_hard_exc
590 ################################################################################
592 def result_join (original, sep = '\t'):
594 for i in xrange(len(original)):
595 if original[i] == None:
598 list.append(original[i])
599 return sep.join(list)
601 ################################################################################
603 def prefix_multi_line_string(str, prefix, include_blank_lines=0):
605 for line in str.split('\n'):
607 if line or include_blank_lines:
608 out += "%s%s\n" % (prefix, line)
609 # Strip trailing new line
614 ################################################################################
616 def validate_changes_file_arg(filename, require_changes=1):
617 """'filename' is either a .changes or .dak file. If 'filename' is a
618 .dak file, it's changed to be the corresponding .changes file. The
619 function then checks if the .changes file a) exists and b) is
620 readable and returns the .changes filename if so. If there's a
621 problem, the next action depends on the option 'require_changes'
624 o If 'require_changes' == -1, errors are ignored and the .changes
625 filename is returned.
626 o If 'require_changes' == 0, a warning is given and 'None' is returned.
627 o If 'require_changes' == 1, a fatal error is raised.
631 orig_filename = filename
632 if filename.endswith(".dak"):
633 filename = filename[:-4]+".changes"
635 if not filename.endswith(".changes"):
636 error = "invalid file type; not a changes file"
638 if not os.access(filename,os.R_OK):
639 if os.path.exists(filename):
640 error = "permission denied"
642 error = "file not found"
645 if require_changes == 1:
646 fubar("%s: %s." % (orig_filename, error))
647 elif require_changes == 0:
648 warn("Skipping %s - %s" % (orig_filename, error))
650 else: # We only care about the .dak file
655 ################################################################################
658 return (arch != "source" and arch != "all")
660 ################################################################################
662 def join_with_commas_and(list):
663 if len(list) == 0: return "nothing"
664 if len(list) == 1: return list[0]
665 return ", ".join(list[:-1]) + " and " + list[-1]
667 ################################################################################
672 (pkg, version, constraint) = atom
674 pp_dep = "%s (%s %s)" % (pkg, constraint, version)
677 pp_deps.append(pp_dep)
678 return " |".join(pp_deps)
680 ################################################################################
685 ################################################################################
687 # Handle -a, -c and -s arguments; returns them as SQL constraints
688 def parse_args(Options):
692 for suite in split_args(Options["Suite"]):
693 suite_id = database.get_suite_id(suite)
695 warn("suite '%s' not recognised." % (suite))
697 suite_ids_list.append(suite_id)
699 con_suites = "AND su.id IN (%s)" % ", ".join([ str(i) for i in suite_ids_list ])
701 fubar("No valid suite given.")
706 if Options["Component"]:
707 component_ids_list = []
708 for component in split_args(Options["Component"]):
709 component_id = database.get_component_id(component)
710 if component_id == -1:
711 warn("component '%s' not recognised." % (component))
713 component_ids_list.append(component_id)
714 if component_ids_list:
715 con_components = "AND c.id IN (%s)" % ", ".join([ str(i) for i in component_ids_list ])
717 fubar("No valid component given.")
721 # Process architecture
722 con_architectures = ""
723 if Options["Architecture"]:
726 for architecture in split_args(Options["Architecture"]):
727 if architecture == "source":
730 architecture_id = database.get_architecture_id(architecture)
731 if architecture_id == -1:
732 warn("architecture '%s' not recognised." % (architecture))
734 arch_ids_list.append(architecture_id)
736 con_architectures = "AND a.id IN (%s)" % ", ".join([ str(i) for i in arch_ids_list ])
739 fubar("No valid architecture given.")
743 return (con_suites, con_architectures, con_components, check_source)
745 ################################################################################
747 # Inspired(tm) by Bryn Keller's print_exc_plus (See
748 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52215)
751 tb = sys.exc_info()[2]
760 traceback.print_exc()
762 print "\nFrame %s in %s at line %s" % (frame.f_code.co_name,
763 frame.f_code.co_filename,
765 for key, value in frame.f_locals.items():
766 print "\t%20s = " % key,
770 print "<unable to print>"
772 ################################################################################
774 def try_with_debug(function):
782 ################################################################################
784 # Function for use in sorting lists of architectures.
785 # Sorts normally except that 'source' dominates all others.
787 def arch_compare_sw (a, b):
788 if a == "source" and b == "source":
797 ################################################################################
799 # Split command line arguments which can be separated by either commas
800 # or whitespace. If dwim is set, it will complain about string ending
801 # in comma since this usually means someone did 'dak ls -a i386, m68k
802 # foo' or something and the inevitable confusion resulting from 'm68k'
803 # being treated as an argument is undesirable.
805 def split_args (s, dwim=1):
806 if s.find(",") == -1:
809 if s[-1:] == "," and dwim:
810 fubar("split_args: found trailing comma, spurious space maybe?")
813 ################################################################################
815 def Dict(**dict): return dict
817 ########################################
819 # Our very own version of commands.getouputstatus(), hacked to support
821 def gpgv_get_status_output(cmd, status_read, status_write):
822 cmd = ['/bin/sh', '-c', cmd]
823 p2cread, p2cwrite = os.pipe()
824 c2pread, c2pwrite = os.pipe()
825 errout, errin = os.pipe()
835 for i in range(3, 256):
836 if i != status_write:
842 os.execvp(cmd[0], cmd)
848 os.dup2(c2pread, c2pwrite)
849 os.dup2(errout, errin)
853 i, o, e = select.select([c2pwrite, errin, status_read], [], [])
856 r = os.read(fd, 8196)
859 if fd == c2pwrite or fd == errin:
861 elif fd == status_read:
864 fubar("Unexpected file descriptor [%s] returned from select\n" % (fd))
866 pid, exit_status = os.waitpid(pid, 0)
868 os.close(status_write)
869 os.close(status_read)
879 return output, status, exit_status
881 ################################################################################
883 def process_gpgv_output(status):
884 # Process the status-fd output
887 for line in status.split('\n'):
893 internal_error += "gpgv status line is malformed (< 2 atoms) ['%s'].\n" % (line)
895 (gnupg, keyword) = split[:2]
896 if gnupg != "[GNUPG:]":
897 internal_error += "gpgv status line is malformed (incorrect prefix '%s').\n" % (gnupg)
900 if keywords.has_key(keyword) and keyword not in [ "NODATA", "SIGEXPIRED", "KEYEXPIRED" ]:
901 internal_error += "found duplicate status token ('%s').\n" % (keyword)
904 keywords[keyword] = args
906 return (keywords, internal_error)
908 ################################################################################
910 def retrieve_key (filename, keyserver=None, keyring=None):
911 """Retrieve the key that signed 'filename' from 'keyserver' and
912 add it to 'keyring'. Returns nothing on success, or an error message
915 # Defaults for keyserver and keyring
917 keyserver = Cnf["Dinstall::KeyServer"]
919 keyring = Cnf.ValueList("Dinstall::GPGKeyring")[0]
921 # Ensure the filename contains no shell meta-characters or other badness
922 if not re_taint_free.match(filename):
923 return "%s: tainted filename" % (filename)
925 # Invoke gpgv on the file
926 status_read, status_write = os.pipe();
927 cmd = "gpgv --status-fd %s --keyring /dev/null %s" % (status_write, filename)
928 (_, status, _) = gpgv_get_status_output(cmd, status_read, status_write)
930 # Process the status-fd output
931 (keywords, internal_error) = process_gpgv_output(status)
933 return internal_error
935 if not keywords.has_key("NO_PUBKEY"):
936 return "didn't find expected NO_PUBKEY in gpgv status-fd output"
938 fingerprint = keywords["NO_PUBKEY"][0]
939 # XXX - gpg sucks. You can't use --secret-keyring=/dev/null as
940 # it'll try to create a lockfile in /dev. A better solution might
941 # be a tempfile or something.
942 cmd = "gpg --no-default-keyring --secret-keyring=%s --no-options" \
943 % (Cnf["Dinstall::SigningKeyring"])
944 cmd += " --keyring %s --keyserver %s --recv-key %s" \
945 % (keyring, keyserver, fingerprint)
946 (result, output) = commands.getstatusoutput(cmd)
948 return "'%s' failed with exit code %s" % (cmd, result)
952 ################################################################################
954 def gpg_keyring_args(keyrings=None):
956 keyrings = Cnf.ValueList("Dinstall::GPGKeyring")
958 return " ".join(["--keyring %s" % x for x in keyrings])
960 ################################################################################
962 def check_signature (sig_filename, reject, data_filename="", keyrings=None, autofetch=None):
963 """Check the signature of a file and return the fingerprint if the
964 signature is valid or 'None' if it's not. The first argument is the
965 filename whose signature should be checked. The second argument is a
966 reject function and is called when an error is found. The reject()
967 function must allow for two arguments: the first is the error message,
968 the second is an optional prefix string. It's possible for reject()
969 to be called more than once during an invocation of check_signature().
970 The third argument is optional and is the name of the files the
971 detached signature applies to. The fourth argument is optional and is
972 a *list* of keyrings to use. 'autofetch' can either be None, True or
973 False. If None, the default behaviour specified in the config will be
976 # Ensure the filename contains no shell meta-characters or other badness
977 if not re_taint_free.match(sig_filename):
978 reject("!!WARNING!! tainted signature filename: '%s'." % (sig_filename))
981 if data_filename and not re_taint_free.match(data_filename):
982 reject("!!WARNING!! tainted data filename: '%s'." % (data_filename))
986 keyrings = Cnf.ValueList("Dinstall::GPGKeyring")
988 # Autofetch the signing key if that's enabled
989 if autofetch == None:
990 autofetch = Cnf.get("Dinstall::KeyAutoFetch")
992 error_msg = retrieve_key(sig_filename)
997 # Build the command line
998 status_read, status_write = os.pipe();
999 cmd = "gpgv --status-fd %s %s %s %s" % (
1000 status_write, gpg_keyring_args(keyrings), sig_filename, data_filename)
1002 # Invoke gpgv on the file
1003 (output, status, exit_status) = gpgv_get_status_output(cmd, status_read, status_write)
1005 # Process the status-fd output
1006 (keywords, internal_error) = process_gpgv_output(status)
1008 # If we failed to parse the status-fd output, let's just whine and bail now
1010 reject("internal error while performing signature check on %s." % (sig_filename))
1011 reject(internal_error, "")
1012 reject("Please report the above errors to the Archive maintainers by replying to this mail.", "")
1016 # Now check for obviously bad things in the processed output
1017 if keywords.has_key("KEYREVOKED"):
1018 reject("The key used to sign %s has been revoked." % (sig_filename))
1020 if keywords.has_key("BADSIG"):
1021 reject("bad signature on %s." % (sig_filename))
1023 if keywords.has_key("ERRSIG") and not keywords.has_key("NO_PUBKEY"):
1024 reject("failed to check signature on %s." % (sig_filename))
1026 if keywords.has_key("NO_PUBKEY"):
1027 args = keywords["NO_PUBKEY"]
1030 reject("The key (0x%s) used to sign %s wasn't found in the keyring(s)." % (key, sig_filename))
1032 if keywords.has_key("BADARMOR"):
1033 reject("ASCII armour of signature was corrupt in %s." % (sig_filename))
1035 if keywords.has_key("NODATA"):
1036 reject("no signature found in %s." % (sig_filename))
1038 if keywords.has_key("KEYEXPIRED") and not keywords.has_key("GOODSIG"):
1039 args = keywords["KEYEXPIRED"]
1042 reject("The key (0x%s) used to sign %s has expired." % (key, sig_filename))
1048 # Next check gpgv exited with a zero return code
1050 reject("gpgv failed while checking %s." % (sig_filename))
1052 reject(prefix_multi_line_string(status, " [GPG status-fd output:] "), "")
1054 reject(prefix_multi_line_string(output, " [GPG output:] "), "")
1057 # Sanity check the good stuff we expect
1058 if not keywords.has_key("VALIDSIG"):
1059 reject("signature on %s does not appear to be valid [No VALIDSIG]." % (sig_filename))
1062 args = keywords["VALIDSIG"]
1064 reject("internal error while checking signature on %s." % (sig_filename))
1067 fingerprint = args[0]
1068 if not keywords.has_key("GOODSIG"):
1069 reject("signature on %s does not appear to be valid [No GOODSIG]." % (sig_filename))
1071 if not keywords.has_key("SIG_ID"):
1072 reject("signature on %s does not appear to be valid [No SIG_ID]." % (sig_filename))
1075 # Finally ensure there's not something we don't recognise
1076 known_keywords = Dict(VALIDSIG="",SIG_ID="",GOODSIG="",BADSIG="",ERRSIG="",
1077 SIGEXPIRED="",KEYREVOKED="",NO_PUBKEY="",BADARMOR="",
1078 NODATA="",NOTATION_DATA="",NOTATION_NAME="",KEYEXPIRED="")
1080 for keyword in keywords.keys():
1081 if not known_keywords.has_key(keyword):
1082 reject("found unknown status token '%s' from gpgv with args '%r' in %s." % (keyword, keywords[keyword], sig_filename))
1090 ################################################################################
1092 # Inspired(tm) by http://www.zopelabs.com/cookbook/1022242603
1094 def wrap(paragraph, max_length, prefix=""):
1098 words = paragraph.split()
1101 word_size = len(word)
1102 if word_size > max_length:
1104 s += line + '\n' + prefix
1105 s += word + '\n' + prefix
1108 new_length = len(line) + word_size + 1
1109 if new_length > max_length:
1110 s += line + '\n' + prefix
1123 ################################################################################
1125 # Relativize an absolute symlink from 'src' -> 'dest' relative to 'root'.
1126 # Returns fixed 'src'
1127 def clean_symlink (src, dest, root):
1128 src = src.replace(root, '', 1)
1129 dest = dest.replace(root, '', 1)
1130 dest = os.path.dirname(dest)
1131 new_src = '../' * len(dest.split('/'))
1132 return new_src + src
1134 ################################################################################
1136 def temp_filename(directory=None, dotprefix=None, perms=0700):
1137 """Return a secure and unique filename by pre-creating it.
1138 If 'directory' is non-null, it will be the directory the file is pre-created in.
1139 If 'dotprefix' is non-null, the filename will be prefixed with a '.'."""
1142 old_tempdir = tempfile.tempdir
1143 tempfile.tempdir = directory
1145 filename = tempfile.mktemp()
1148 filename = "%s/.%s" % (os.path.dirname(filename), os.path.basename(filename))
1149 fd = os.open(filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, perms)
1153 tempfile.tempdir = old_tempdir
1157 ################################################################################
1159 # checks if the user part of the email is listed in the alias file
1161 def is_email_alias(email):
1162 aliasfn = which_alias_file()
1163 uid = email.split('@')[0]
1166 for l in open(aliasfn):
1167 if l.startswith(uid+': '):
1171 ################################################################################
1175 Cnf = apt_pkg.newConfiguration()
1176 apt_pkg.ReadConfigFileISC(Cnf,default_config)
1178 if which_conf_file() != default_config:
1179 apt_pkg.ReadConfigFileISC(Cnf,which_conf_file())
1181 ################################################################################