X-Git-Url: https://git.decadent.org.uk/gitweb/?a=blobdiff_plain;f=utils.py;h=8445fd4d664289f1f6f9ad91335626546a27d2b5;hb=3686a00f1001f2d5692fa5e706b898053e39191a;hp=1be0b7e0f0dde198115e81bac3fdd3ef75f66b87;hpb=d3c6467a180f457b80b8afeefec27f4a7347ff8a;p=dak.git diff --git a/utils.py b/utils.py index 1be0b7e0..8445fd4d 100644 --- a/utils.py +++ b/utils.py @@ -1,8 +1,10 @@ #!/usr/bin/env python # Utility functions -# Copyright (C) 2000, 2001, 2002, 2003 James Troup -# $Id: utils.py,v 1.56 2003-02-21 19:20:00 troup Exp $ +# Copyright (C) 2000, 2001, 2002, 2003, 2004 James Troup +# $Id: utils.py,v 1.65 2004-04-01 17:13:10 troup Exp $ + +################################################################################ # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -43,28 +45,27 @@ re_parse_maintainer = re.compile(r"^\s*(\S.*\S)\s*\<([^\> \t]+)\>"); changes_parse_error_exc = "Can't parse line in .changes file"; invalid_dsc_format_exc = "Invalid .dsc file"; nk_format_exc = "Unknown Format: in .changes file"; -no_files_exc = "No Files: field in .dsc file."; +no_files_exc = "No Files: field in .dsc or .changes file."; cant_open_exc = "Can't read file."; unknown_hostname_exc = "Unknown hostname"; cant_overwrite_exc = "Permission denied; can't overwrite existent file." file_exists_exc = "Destination file exists"; -send_mail_invalid_args_exc = "Both arguments are non-null."; sendmail_failed_exc = "Sendmail invocation failed"; tried_too_hard_exc = "Tried too hard to find a free filename."; default_config = "/etc/katie/katie.conf"; default_apt_config = "/etc/katie/apt.conf"; -###################################################################################### +################################################################################ def open_file(filename, mode='r'): try: f = open(filename, mode); except IOError: - raise cant_open_exc, filename + raise cant_open_exc, filename; return f -###################################################################################### +################################################################################ def our_raw_input(prompt=""): if prompt: @@ -72,12 +73,12 @@ def our_raw_input(prompt=""): sys.stdout.flush(); try: ret = raw_input(); - return ret + return ret; except EOFError: - sys.stderr.write('\nUser interrupt (^D).\n'); + sys.stderr.write("\nUser interrupt (^D).\n"); raise SystemExit; -###################################################################################### +################################################################################ def str_isnum (s): for c in s: @@ -85,14 +86,14 @@ def str_isnum (s): return 0; return 1; -###################################################################################### +################################################################################ def extract_component_from_section(section): component = ""; if section.find('/') != -1: component = section.split('/')[0]; - if component.lower() == "non-us" and section.count('/') > 0: + if component.lower() == "non-us" and section.find('/') != -1: s = component + '/' + section.split('/')[1]; if Cnf.has_key("Component::%s" % s): # Avoid e.g. non-US/libs component = s; @@ -115,25 +116,30 @@ def extract_component_from_section(section): return (section, component); -###################################################################################### +################################################################################ + +# Parses a changes file and returns a dictionary where each field is a +# key. The mandatory first argument is the filename of the .changes +# file. -# dsc_whitespace_rules turns on strict format checking to avoid +# dsc_whitespace_rules is an optional boolean argument which defaults +# to off. If true, it turns on strict format checking to avoid # allowing in source packages which are unextracable by the # inappropriately fragile dpkg-source. # # The rules are: # +# o The PGP header consists of "-----BEGIN PGP SIGNED MESSAGE-----" +# followed by any PGP header data and must end with a blank line. # -# o The PGP header consists of "-----BEGIN PGP SIGNED MESSAGE-----" -# followed by any PGP header data and must end with a blank line. -# -# o The data section must end with a blank line and must be followed by -# "-----BEGIN PGP SIGNATURE-----". +# o The data section must end with a blank line and must be followed by +# "-----BEGIN PGP SIGNATURE-----". def parse_changes(filename, dsc_whitespace_rules=0): - changes_in = open_file(filename); error = ""; changes = {}; + + changes_in = open_file(filename); lines = changes_in.readlines(); if not lines: @@ -149,31 +155,36 @@ def parse_changes(filename, dsc_whitespace_rules=0): inside_signature = 0; - indices = indexed_lines.keys() + num_of_lines = len(indexed_lines.keys()); index = 0; first = -1; - while index < max(indices): + while index < num_of_lines: index += 1; line = indexed_lines[index]; if line == "": if dsc_whitespace_rules: index += 1; - if index > max(indices): + if index > num_of_lines: raise invalid_dsc_format_exc, index; line = indexed_lines[index]; if not line.startswith("-----BEGIN PGP SIGNATURE"): raise invalid_dsc_format_exc, index; inside_signature = 0; break; + else: + continue; if line.startswith("-----BEGIN PGP SIGNATURE"): break; if line.startswith("-----BEGIN PGP SIGNED MESSAGE"): + inside_signature = 1; if dsc_whitespace_rules: - inside_signature = 1; - while index < max(indices) and line != "": + while index < num_of_lines and line != "": index += 1; line = indexed_lines[index]; continue; + # If we're not inside the signed data, don't process anything + if not inside_signature: + continue; slf = re_single_line_field.match(line); if slf: field = slf.groups()[0].lower(); @@ -200,54 +211,56 @@ def parse_changes(filename, dsc_whitespace_rules=0): changes_in.close(); changes["filecontents"] = "".join(lines); - if error != "": + if error: raise changes_parse_error_exc, error; return changes; -###################################################################################### +################################################################################ # Dropped support for 1.4 and ``buggy dchanges 3.4'' (?!) compared to di.pl def build_file_list(changes, is_a_dsc=0): - files = {} - format = changes.get("format", "") + files = {}; + + # Make sure we have a Files: field to parse... + if not changes.has_key("files"): + raise no_files_exc; + + # Make sure we recognise the format of the Files: field + format = changes.get("format", ""); if format != "": - format = float(format) + format = float(format); if not is_a_dsc and (format < 1.5 or format > 2.0): raise nk_format_exc, format; - # No really, this has happened. Think 0 length .dsc file. - if not changes.has_key("files"): - raise no_files_exc - - for i in changes["files"].split("\n"): - if i == "": - break + # Parse each entry/line: + for i in changes["files"].split('\n'): + if not i: + break; s = i.split(); section = priority = ""; try: if is_a_dsc: - (md5, size, name) = s + (md5, size, name) = s; else: - (md5, size, section, priority, name) = s + (md5, size, section, priority, name) = s; except ValueError: - raise changes_parse_error_exc, i + raise changes_parse_error_exc, i; - if section == "": section = "-" - if priority == "": priority = "-" + if section == "": + section = "-"; + if priority == "": + priority = "-"; (section, component) = extract_component_from_section(section); - files[name] = { "md5sum" : md5, - "size" : size, - "section": section, - "priority": priority, - "component": component } + files[name] = Dict(md5sum=md5, size=size, section=section, + priority=priority, component=component); return files -###################################################################################### +################################################################################ # Fix the `Maintainer:' field to be an RFC822 compatible address. # cf. Debian Policy Manual (D.2.4) @@ -261,23 +274,19 @@ def fix_maintainer (maintainer): rfc822 = maintainer; name = ""; email = ""; - if m != None and len(m.groups()) == 2: + if m and len(m.groups()) == 2: name = m.group(1); email = m.group(2); if name.find(',') != -1 or name.find('.') != -1: rfc822 = "%s (%s)" % (email, name); return (rfc822, name, email) -###################################################################################### +################################################################################ # sendmail wrapper, takes _either_ a message string or a file as arguments -def send_mail (message, filename): - # Sanity check arguments - if message != "" and filename != "": - raise send_mail_invalid_args_exc; - +def send_mail (message, filename=""): # If we've been passed a string dump it into a temporary file - if message != "": + if message: filename = tempfile.mktemp(); fd = os.open(filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0700); os.write (fd, message); @@ -289,22 +298,22 @@ def send_mail (message, filename): raise sendmail_failed_exc, output; # Clean up any temporary files - if message !="": + if message: os.unlink (filename); -###################################################################################### +################################################################################ def poolify (source, component): - if component != "": + if component: component += '/'; # FIXME: this is nasty - component = component.lower().replace('non-us/', 'non-US/'); + component = component.lower().replace("non-us/", "non-US/"); if source[:3] == "lib": return component + source[:4] + '/' + source + '/' else: return component + source[:1] + '/' + source + '/' -###################################################################################### +################################################################################ def move (src, dest, overwrite = 0, perms = 0664): if os.path.exists(dest) and os.path.isdir(dest): @@ -321,10 +330,10 @@ def move (src, dest, overwrite = 0, perms = 0664): # Don't overwrite unless forced to if os.path.exists(dest): if not overwrite: - raise file_exists_exc; + fubar("Can't move %s to %s - file already exists." % (src, dest)); else: if not os.access(dest, os.W_OK): - raise cant_overwrite_exc + fubar("Can't move %s to %s - can't write to existing file." % (src, dest)); shutil.copy2(src, dest); os.chmod(dest, perms); os.unlink(src); @@ -351,7 +360,7 @@ def copy (src, dest, overwrite = 0, perms = 0664): shutil.copy2(src, dest); os.chmod(dest, perms); -###################################################################################### +################################################################################ def where_am_i (): res = socket.gethostbyaddr(socket.gethostname()); @@ -375,7 +384,7 @@ def which_apt_conf_file (): else: return default_apt_config; -###################################################################################### +################################################################################ # Escape characters which have meaning to SQL's regex comparison operator ('~') # (woefully incomplete) @@ -385,7 +394,7 @@ def regex_safe (s): s = s.replace('.', '\\\\.'); return s -###################################################################################### +################################################################################ # Perform a substition of template def TemplateSubst(map, filename): @@ -396,7 +405,7 @@ def TemplateSubst(map, filename): file.close(); return template; -###################################################################################### +################################################################################ def fubar(msg, exit_code=1): sys.stderr.write("E: %s\n" % (msg)); @@ -405,14 +414,14 @@ def fubar(msg, exit_code=1): def warn(msg): sys.stderr.write("W: %s\n" % (msg)); -###################################################################################### +################################################################################ # Returns the user name with a laughable attempt at rfc822 conformancy # (read: removing stray periods). def whoami (): return pwd.getpwuid(os.getuid())[4].split(',')[0].replace('.', ''); -###################################################################################### +################################################################################ def size_type (c): t = " b"; @@ -427,12 +436,12 @@ def size_type (c): ################################################################################ def cc_fix_changes (changes): - o = changes.get("architecture", "") - if o != "": - del changes["architecture"] - changes["architecture"] = {} + o = changes.get("architecture", ""); + if o: + del changes["architecture"]; + changes["architecture"] = {}; for j in o.split(): - changes["architecture"][j] = 1 + changes["architecture"][j] = 1; # Sort by source name, source version, 'have source', and then by filename def changes_compare (a, b): @@ -561,7 +570,7 @@ def parse_args(Options): # Process suite if Options["Suite"]: suite_ids_list = []; - for suite in Options["Suite"].split(): + for suite in split_args(Options["Suite"]): suite_id = db_access.get_suite_id(suite); if suite_id == -1: warn("suite '%s' not recognised." % (suite)); @@ -577,7 +586,7 @@ def parse_args(Options): # Process component if Options["Component"]: component_ids_list = []; - for component in Options["Component"].split(): + for component in split_args(Options["Component"]): component_id = db_access.get_component_id(component); if component_id == -1: warn("component '%s' not recognised." % (component)); @@ -595,7 +604,7 @@ def parse_args(Options): if Options["Architecture"]: arch_ids_list = []; check_source = 0; - for architecture in Options["Architecture"].split(): + for architecture in split_args(Options["Architecture"]): if architecture == "source": check_source = 1; else: @@ -668,6 +677,22 @@ def arch_compare_sw (a, b): ################################################################################ +# Split command line arguments which can be separated by either commas +# or whitespace. If dwim is set, it will complain about string ending +# in comma since this usually means someone did 'madison -a i386, m68k +# foo' or something and the inevitable confusion resulting from 'm68k' +# being treated as an argument is undesirable. + +def split_args (s, dwim=1): + if s.find(",") == -1: + return s.split(); + else: + if s[-1:] == "," and dwim: + fubar("split_args: found trailing comma, spurious space maybe?"); + return s.split(","); + +################################################################################ + def Dict(**dict): return dict ######################################## @@ -699,7 +724,7 @@ def gpgv_get_status_output(cmd, status_read, status_write): finally: os._exit(1); - # parent + # Parent os.close(p2cread) os.dup2(c2pread, c2pwrite); os.dup2(errout, errin); @@ -743,8 +768,8 @@ signature is valid or 'None' if it's not. The first argument is the filename whose signature should be checked. The second argument is a reject function and is called when an error is found. The reject() function must allow for two arguments: the first is the error message, -the second is an optional prefix string. It is possible that reject() -is called more than once during an invocation of check_signature().""" +the second is an optional prefix string. It's possible for reject() +to be called more than once during an invocation of check_signature().""" # Ensure the filename contains no shell meta-characters or other badness if not re_taint_free.match(os.path.basename(filename)): @@ -788,10 +813,10 @@ is called more than once during an invocation of check_signature().""" # Now check for obviously bad things in the processed output if keywords.has_key("SIGEXPIRED"): - reject("key used to sign %s has expired." % (filename)); + reject("The key used to sign %s has expired." % (filename)); bad = 1; if keywords.has_key("KEYREVOKED"): - reject("key used to sign %s has been revoked." % (filename)); + reject("The key used to sign %s has been revoked." % (filename)); bad = 1; if keywords.has_key("BADSIG"): reject("bad signature on %s." % (filename)); @@ -800,10 +825,13 @@ is called more than once during an invocation of check_signature().""" reject("failed to check signature on %s." % (filename)); bad = 1; if keywords.has_key("NO_PUBKEY"): - reject("key used to sign %s not found in keyring." % (filename)); + args = keywords["NO_PUBKEY"]; + if len(args) >= 1: + key = args[0]; + reject("The key (0x%s) used to sign %s wasn't found in the keyring(s)." % (key, filename)); bad = 1; if keywords.has_key("BADARMOR"): - reject("ascii armour of signature was corrupt in %s." % (filename)); + reject("ASCII armour of signature was corrupt in %s." % (filename)); bad = 1; if keywords.has_key("NODATA"): reject("no signature found in %s." % (filename)); @@ -856,12 +884,79 @@ is called more than once during an invocation of check_signature().""" ################################################################################ -apt_pkg.init() +# Inspired(tm) by http://www.zopelabs.com/cookbook/1022242603 + +def wrap(paragraph, max_length, prefix=""): + line = ""; + s = ""; + have_started = 0; + words = paragraph.split(); + + for word in words: + word_size = len(word); + if word_size > max_length: + if have_started: + s += line + '\n' + prefix; + s += word + '\n' + prefix; + else: + if have_started: + new_length = len(line) + word_size + 1; + if new_length > max_length: + s += line + '\n' + prefix; + line = word; + else: + line += ' ' + word; + else: + line = word; + have_started = 1; + + if have_started: + s += line; + + return s; + +################################################################################ + +# Relativize an absolute symlink from 'src' -> 'dest' relative to 'root'. +# Returns fixed 'src' +def clean_symlink (src, dest, root): + src = src.replace(root, '', 1); + dest = dest.replace(root, '', 1); + dest = os.path.dirname(dest); + new_src = '../' * len(dest.split('/')); + return new_src + src; + +################################################################################ + +def temp_filename(directory=None, dotprefix=None, perms=0700): + """Return a secure and unique filename by pre-creating it. +If 'directory' is non-null, it will be the directory the file is pre-created in. +If 'dotprefix' is non-null, the filename will be prefixed with a '.'.""" + + if directory: + old_tempdir = tempfile.tempdir; + tempfile.tempdir = directory; + + filename = tempfile.mktemp(); + + if dotprefix: + filename = "%s/.%s" % (os.path.dirname(filename), os.path.basename(filename)); + fd = os.open(filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, perms); + os.close(fd); + + if directory: + tempfile.tempdir = old_tempdir; + + return filename; + +################################################################################ + +apt_pkg.init(); Cnf = apt_pkg.newConfiguration(); apt_pkg.ReadConfigFileISC(Cnf,default_config); if which_conf_file() != default_config: - apt_pkg.ReadConfigFileISC(Cnf,which_conf_file()) + apt_pkg.ReadConfigFileISC(Cnf,which_conf_file()); ################################################################################