utils.py 37.6 KB
Newer Older
1 2
#!/usr/bin/env python

J
James Troup 已提交
3
# Utility functions
4
# Copyright (C) 2000, 2001, 2002, 2003, 2004, 2005, 2006  James Troup <james@nocrew.org>
5 6

################################################################################
J
James Troup 已提交
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

# 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
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

22 23
################################################################################

24
import codecs, commands, email.Header, os, pwd, re, select, socket, shutil, \
25
       sys, tempfile, traceback
26
import apt_pkg
J
James Troup 已提交
27
import database
28 29

################################################################################
J
James Troup 已提交
30 31

re_comments = re.compile(r"\#.*")
32 33
re_no_epoch = re.compile(r"^\d+\:")
re_no_revision = re.compile(r"-[^-]+$")
J
James Troup 已提交
34 35
re_arch_from_filename = re.compile(r"/binary-[^/]+/")
re_extract_src_version = re.compile (r"(\S+)\s*\((.*)\)")
36 37
re_isadeb = re.compile (r"(.+?)_(.+?)_(.+)\.u?deb$")
re_issource = re.compile (r"(.+)_(.+?)\.(orig\.tar\.gz|diff\.gz|tar\.gz|dsc)$")
38

39 40 41
re_single_line_field = re.compile(r"^(\S*)\s*:\s*(.*)")
re_multi_line_field = re.compile(r"^\s(.*)")
re_taint_free = re.compile(r"^[-+~/\.\w]+$")
42

43
re_parse_maintainer = re.compile(r"^\s*(\S.*\S)\s*\<([^\>]+)\>")
J
James Troup 已提交
44

45
re_srchasver = re.compile(r"^(\S+)\s+\((\S+)\)$")
46
re_verwithext = re.compile(r"^(\d+)(?:\.(\d+))(?:\s+\((\S+)\))?$")
47

48 49 50 51 52 53
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 or .changes file."
cant_open_exc = "Can't open file"
unknown_hostname_exc = "Unknown hostname"
J
James Troup 已提交
54
cant_overwrite_exc = "Permission denied; can't overwrite existent file."
55 56 57
file_exists_exc = "Destination file exists"
sendmail_failed_exc = "Sendmail invocation failed"
tried_too_hard_exc = "Tried too hard to find a free filename."
58

59 60
default_config = "/etc/dak/dak.conf"
default_apt_config = "/etc/dak/apt.conf"
J
James Troup 已提交
61

62
################################################################################
J
James Troup 已提交
63

64 65
class Error(Exception):
    """Base class for exceptions in this module."""
66
    pass
67 68 69 70 71 72 73 74 75

class ParseMaintError(Error):
    """Exception raised for errors in parsing a maintainer field.

    Attributes:
       message -- explanation of the error
    """

    def __init__(self, message):
76 77
        self.args = message,
        self.message = message
78 79 80

################################################################################

81
def open_file(filename, mode='r'):
J
James Troup 已提交
82
    try:
83
	f = open(filename, mode)
J
James Troup 已提交
84
    except IOError:
85
        raise cant_open_exc, filename
J
James Troup 已提交
86 87
    return f

88
################################################################################
J
James Troup 已提交
89

90 91
def our_raw_input(prompt=""):
    if prompt:
92 93
        sys.stdout.write(prompt)
    sys.stdout.flush()
J
James Troup 已提交
94
    try:
95 96
        ret = raw_input()
        return ret
J
James Troup 已提交
97
    except EOFError:
98 99
        sys.stderr.write("\nUser interrupt (^D).\n")
        raise SystemExit
J
James Troup 已提交
100

101
################################################################################
J
James Troup 已提交
102

J
James Troup 已提交
103
def extract_component_from_section(section):
104
    component = ""
J
James Troup 已提交
105

106
    if section.find('/') != -1:
107
        component = section.split('/')[0]
108 109

    # Expand default component
J
James Troup 已提交
110
    if component == "":
111
        if Cnf.has_key("Component::%s" % section):
112
            component = section
113
        else:
114
            component = "main"
J
James Troup 已提交
115

116
    return (section, component)
J
James Troup 已提交
117

118 119
################################################################################

120 121 122 123
def parse_changes(filename, signing_rules=0):
    """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.
J
James Troup 已提交
124

125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
signing_rules is an optional argument:

 o If signing_rules == -1, no signature is required.
 o If signing_rules == 0 (the default), a signature is required.
 o If signing_rules == 1, it turns on the same strict format checking
   as dpkg-source.

The rules for (signing_rules == 1)-mode 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 data section must end with a blank line and must be followed by
    "-----BEGIN PGP SIGNATURE-----".
"""
J
James Troup 已提交
140

141 142
    error = ""
    changes = {}
143

144 145
    changes_in = open_file(filename)
    lines = changes_in.readlines()
J
James Troup 已提交
146

147
    if not lines:
148
	raise changes_parse_error_exc, "[Empty changes file]"
149

J
James Troup 已提交
150 151
    # Reindex by line number so we can easily verify the format of
    # .dsc files...
152 153
    index = 0
    indexed_lines = {}
J
James Troup 已提交
154
    for line in lines:
155 156
        index += 1
        indexed_lines[index] = line[:-1]
J
James Troup 已提交
157

158
    inside_signature = 0
J
James Troup 已提交
159

160 161 162
    num_of_lines = len(indexed_lines.keys())
    index = 0
    first = -1
163
    while index < num_of_lines:
164 165
        index += 1
        line = indexed_lines[index]
J
James Troup 已提交
166
        if line == "":
167
            if signing_rules == 1:
168
                index += 1
169
                if index > num_of_lines:
170 171
                    raise invalid_dsc_format_exc, index
                line = indexed_lines[index]
172
                if not line.startswith("-----BEGIN PGP SIGNATURE"):
173 174 175
                    raise invalid_dsc_format_exc, index
                inside_signature = 0
                break
176
            else:
177
                continue
178
        if line.startswith("-----BEGIN PGP SIGNATURE"):
179
            break
180
        if line.startswith("-----BEGIN PGP SIGNED MESSAGE"):
181
            inside_signature = 1
182
            if signing_rules == 1:
183
                while index < num_of_lines and line != "":
184 185 186
                    index += 1
                    line = indexed_lines[index]
            continue
187
        # If we're not inside the signed data, don't process anything
188
        if signing_rules >= 0 and not inside_signature:
189 190
            continue
        slf = re_single_line_field.match(line)
J
James Troup 已提交
191
        if slf:
192 193 194 195
            field = slf.groups()[0].lower()
            changes[field] = slf.groups()[1]
	    first = 1
            continue
J
James Troup 已提交
196
        if line == " .":
197 198 199
            changes[field] += '\n'
            continue
        mlf = re_multi_line_field.match(line)
J
James Troup 已提交
200
        if mlf:
201
            if first == -1:
202
                raise changes_parse_error_exc, "'%s'\n [Multi-line field continuing on from nothing?]" % (line)
203
            if first == 1 and changes[field] != "":
204 205 206 207 208
                changes[field] += '\n'
            first = 0
	    changes[field] += mlf.groups()[0] + '\n'
            continue
	error += line
J
James Troup 已提交
209

210
    if signing_rules == 1 and inside_signature:
211
        raise invalid_dsc_format_exc, index
J
James Troup 已提交
212

213 214
    changes_in.close()
    changes["filecontents"] = "".join(lines)
J
James Troup 已提交
215

216 217 218 219 220 221 222 223
    if changes.has_key("source"):
        # Strip the source version in brackets from the source field,
	# put it in the "source-version" field instead.
        srcver = re_srchasver.search(changes["source"])
	if srcver:
            changes["source"] = srcver.group(1)
	    changes["source-version"] = srcver.group(2)

224
    if error:
225
	raise changes_parse_error_exc, error
J
James Troup 已提交
226

227
    return changes
J
James Troup 已提交
228

229
################################################################################
J
James Troup 已提交
230 231 232

# 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"):
234
    files = {}
235 236

    # Make sure we have a Files: field to parse...
237 238
    if not changes.has_key(field):
        raise no_files_exc
239 240

    # Make sure we recognise the format of the Files: field
241 242 243 244 245 246 247
    format = re_verwithext.search(changes.get("format", "0.0"))
    if not format:
        raise nk_format_exc, "%s" % (changes.get("format","0.0"))

    format = format.groups()
    if format[1] == None:
        format = int(float(format[0])), 0, format[2]
248
    else:
249 250 251
        format = int(format[0]), int(format[1]), format[2]
    if format[2] == None:
        format = format[:2]
252 253 254 255 256 257 258 259 260 261 262

    if is_a_dsc:
        if format != (1,0):
            raise nk_format_exc, "%s" % (changes.get("format","0.0"))
    else:
        if (format < (1,5) or format > (1,8)):
            raise nk_format_exc, "%s" % (changes.get("format","0.0"))
	if field != "files" and format < (1,8):
            raise nk_format_exc, "%s" % (changes.get("format","0.0"))

    includes_section = (not is_a_dsc) and field == "files"
J
James Troup 已提交
263

264
    # Parse each entry/line:
265
    for i in changes[field].split('\n'):
266
        if not i:
267 268 269
            break
        s = i.split()
        section = priority = ""
J
sync  
James Troup 已提交
270
        try:
271
            if includes_section:
272
                (md5, size, section, priority, name) = s
273 274
            else:
                (md5, size, name) = s
J
sync  
James Troup 已提交
275
        except ValueError:
276
            raise changes_parse_error_exc, i
J
James Troup 已提交
277

278
        if section == "":
279
            section = "-"
280
        if priority == "":
281
            priority = "-"
J
James Troup 已提交
282

283
        (section, component) = extract_component_from_section(section)
J
James Troup 已提交
284

285
        files[name] = Dict(size=size, section=section,
286
                           priority=priority, component=component)
287
        files[name][hashname] = md5
J
James Troup 已提交
288 289 290

    return files

291
################################################################################
J
James Troup 已提交
292

293 294 295 296
def force_to_utf8(s):
    """Forces a string to UTF-8.  If the string isn't already UTF-8,
it's assumed to be ISO-8859-1."""
    try:
297 298
        unicode(s, 'utf-8')
        return s
299
    except UnicodeError:
300 301
        latin1_s = unicode(s,'iso8859-1')
        return latin1_s.encode('utf-8')
302 303 304 305 306

def rfc2047_encode(s):
    """Encodes a (header) string per RFC2047 if necessary.  If the
string is neither ASCII nor UTF-8, it's assumed to be ISO-8859-1."""
    try:
307
        codecs.lookup('ascii')[1](s)
308
        return s
309
    except UnicodeError:
310
        pass
311
    try:
312
        codecs.lookup('utf-8')[1](s)
313 314
        h = email.Header.Header(s, 'utf-8', 998)
        return str(h)
315
    except UnicodeError:
316 317
        h = email.Header.Header(s, 'iso-8859-1', 998)
        return str(h)
318 319 320 321 322 323

################################################################################

# <Culus> 'The standard sucks, but my tool is supposed to interoperate
#          with it. I know - I'll fix the suckage and make things
#          incompatible!'
J
James Troup 已提交
324

J
James Troup 已提交
325
def fix_maintainer (maintainer):
326 327 328 329 330 331 332 333 334 335 336
    """Parses a Maintainer or Changed-By field and returns:
  (1) an RFC822 compatible version,
  (2) an RFC2047 compatible version,
  (3) the name
  (4) the email

The name is forced to UTF-8 for both (1) and (3).  If the name field
contains '.' or ',' (as allowed by Debian policy), (1) and (2) are
switched to 'email (name)' format."""
    maintainer = maintainer.strip()
    if not maintainer:
337
        return ('', '', '', '')
338

339
    if maintainer.find("<") == -1:
340 341
        email = maintainer
        name = ""
342
    elif (maintainer[0] == "<" and maintainer[-1:] == ">"):
343 344
        email = maintainer[1:-1]
        name = ""
345
    else:
346
        m = re_parse_maintainer.match(maintainer)
347 348
        if not m:
            raise ParseMaintError, "Doesn't parse as a valid Maintainer field."
349 350
        name = m.group(1)
        email = m.group(2)
351 352

    # Get an RFC2047 compliant version of the name
353
    rfc2047_name = rfc2047_encode(name)
354 355

    # Force the name to be UTF-8
356
    name = force_to_utf8(name)
357 358

    if name.find(',') != -1 or name.find('.') != -1:
359 360
        rfc822_maint = "%s (%s)" % (email, name)
        rfc2047_maint = "%s (%s)" % (email, rfc2047_name)
361
    else:
362 363
        rfc822_maint = "%s <%s>" % (name, email)
        rfc2047_maint = "%s <%s>" % (rfc2047_name, email)
364 365 366 367

    if email.find("@") == -1 and email.find("buildd_") != 0:
        raise ParseMaintError, "No @ found in email address part."

368
    return (rfc822_maint, rfc2047_maint, name, email)
J
James Troup 已提交
369

370
################################################################################
J
James Troup 已提交
371 372

# sendmail wrapper, takes _either_ a message string or a file as arguments
373
def send_mail (message, filename=""):
J
James Troup 已提交
374
	# If we've been passed a string dump it into a temporary file
375
	if message:
376 377 378 379
            filename = tempfile.mktemp()
            fd = os.open(filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0700)
            os.write (fd, message)
            os.close (fd)
J
James Troup 已提交
380

J
James Troup 已提交
381
	# Invoke sendmail
382
	(result, output) = commands.getstatusoutput("%s < %s" % (Cnf["Dinstall::SendmailCommand"], filename))
J
James Troup 已提交
383
	if (result != 0):
384
            raise sendmail_failed_exc, output
J
James Troup 已提交
385

J
James Troup 已提交
386
	# Clean up any temporary files
387
	if message:
388
            os.unlink (filename)
J
James Troup 已提交
389

390
################################################################################
J
James Troup 已提交
391 392

def poolify (source, component):
393
    if component:
394
	component += '/'
J
James Troup 已提交
395 396 397 398 399
    if source[:3] == "lib":
	return component + source[:4] + '/' + source + '/'
    else:
	return component + source[:1] + '/' + source + '/'

400
################################################################################
J
James Troup 已提交
401

402
def move (src, dest, overwrite = 0, perms = 0664):
J
James Troup 已提交
403
    if os.path.exists(dest) and os.path.isdir(dest):
404
	dest_dir = dest
J
James Troup 已提交
405
    else:
406
	dest_dir = os.path.dirname(dest)
J
James Troup 已提交
407
    if not os.path.exists(dest_dir):
408 409 410 411
	umask = os.umask(00000)
	os.makedirs(dest_dir, 02775)
	os.umask(umask)
    #print "Moving %s to %s..." % (src, dest)
J
James Troup 已提交
412
    if os.path.exists(dest) and os.path.isdir(dest):
413
	dest += '/' + os.path.basename(src)
414 415 416
    # Don't overwrite unless forced to
    if os.path.exists(dest):
        if not overwrite:
417
            fubar("Can't move %s to %s - file already exists." % (src, dest))
418 419
        else:
            if not os.access(dest, os.W_OK):
420 421 422 423
                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)
J
James Troup 已提交
424

425
def copy (src, dest, overwrite = 0, perms = 0664):
J
James Troup 已提交
426
    if os.path.exists(dest) and os.path.isdir(dest):
427
	dest_dir = dest
J
James Troup 已提交
428
    else:
429
	dest_dir = os.path.dirname(dest)
J
James Troup 已提交
430
    if not os.path.exists(dest_dir):
431 432 433 434
	umask = os.umask(00000)
	os.makedirs(dest_dir, 02775)
	os.umask(umask)
    #print "Copying %s to %s..." % (src, dest)
J
James Troup 已提交
435
    if os.path.exists(dest) and os.path.isdir(dest):
436
	dest += '/' + os.path.basename(src)
437 438 439 440 441 442 443
    # Don't overwrite unless forced to
    if os.path.exists(dest):
        if not overwrite:
            raise file_exists_exc
        else:
            if not os.access(dest, os.W_OK):
                raise cant_overwrite_exc
444 445
    shutil.copy2(src, dest)
    os.chmod(dest, perms)
J
James Troup 已提交
446

447
################################################################################
J
James Troup 已提交
448 449

def where_am_i ():
450 451
    res = socket.gethostbyaddr(socket.gethostname())
    database_hostname = Cnf.get("Config::" + res[0] + "::DatabaseHostname")
452
    if database_hostname:
453
	return database_hostname
J
James Troup 已提交
454
    else:
455
        return res[0]
J
James Troup 已提交
456 457

def which_conf_file ():
458
    res = socket.gethostbyaddr(socket.gethostname())
459 460
    if Cnf.get("Config::" + res[0] + "::DakConfig"):
	return Cnf["Config::" + res[0] + "::DakConfig"]
J
James Troup 已提交
461
    else:
462
	return default_config
463 464

def which_apt_conf_file ():
465
    res = socket.gethostbyaddr(socket.gethostname())
466 467
    if Cnf.get("Config::" + res[0] + "::AptConfig"):
	return Cnf["Config::" + res[0] + "::AptConfig"]
468
    else:
469
	return default_apt_config
470

471
################################################################################
J
James Troup 已提交
472

473 474 475 476
# Escape characters which have meaning to SQL's regex comparison operator ('~')
# (woefully incomplete)

def regex_safe (s):
477 478
    s = s.replace('+', '\\\\+')
    s = s.replace('.', '\\\\.')
479 480
    return s

481
################################################################################
482

J
James Troup 已提交
483
# Perform a substition of template
484
def TemplateSubst(map, filename):
485 486
    file = open_file(filename)
    template = file.read()
487
    for x in map.keys():
488 489 490
        template = template.replace(x,map[x])
    file.close()
    return template
J
James Troup 已提交
491

492
################################################################################
J
James Troup 已提交
493 494

def fubar(msg, exit_code=1):
495 496
    sys.stderr.write("E: %s\n" % (msg))
    sys.exit(exit_code)
J
James Troup 已提交
497 498

def warn(msg):
499
    sys.stderr.write("W: %s\n" % (msg))
J
James Troup 已提交
500

501
################################################################################
J
James Troup 已提交
502

J
James Troup 已提交
503 504 505
# Returns the user name with a laughable attempt at rfc822 conformancy
# (read: removing stray periods).
def whoami ():
506
    return pwd.getpwuid(os.getuid())[4].split(',')[0].replace('.', '')
J
James Troup 已提交
507

508
################################################################################
J
James Troup 已提交
509

J
James Troup 已提交
510
def size_type (c):
511
    t  = " B"
512
    if c > 10240:
513 514
        c = c / 1024
        t = " KB"
515
    if c > 10240:
516 517
        c = c / 1024
        t = " MB"
J
James Troup 已提交
518
    return ("%d%s" % (c, t))
519 520 521 522

################################################################################

def cc_fix_changes (changes):
523
    o = changes.get("architecture", "")
524
    if o:
525 526
        del changes["architecture"]
    changes["architecture"] = {}
527
    for j in o.split():
528
        changes["architecture"][j] = 1
529

530
# Sort by source name, source version, 'have source', and then by filename
531 532
def changes_compare (a, b):
    try:
533
        a_changes = parse_changes(a)
534
    except:
535
        return -1
536 537

    try:
538
        b_changes = parse_changes(b)
539
    except:
540
        return 1
J
James Troup 已提交
541

542 543
    cc_fix_changes (a_changes)
    cc_fix_changes (b_changes)
544 545

    # Sort by source name
546 547 548
    a_source = a_changes.get("source")
    b_source = b_changes.get("source")
    q = cmp (a_source, b_source)
549
    if q:
550
        return q
551 552

    # Sort by source version
553 554 555
    a_version = a_changes.get("version", "0")
    b_version = b_changes.get("version", "0")
    q = apt_pkg.VersionCompare(a_version, b_version)
556
    if q:
557
        return q
558

559
    # Sort by 'have source'
560 561
    a_has_source = a_changes["architecture"].get("source")
    b_has_source = b_changes["architecture"].get("source")
562
    if a_has_source and not b_has_source:
563
        return -1
564
    elif b_has_source and not a_has_source:
565
        return 1
566

567
    # Fall back to sort by filename
568
    return cmp(a, b)
569 570

################################################################################
571 572

def find_next_free (dest, too_many=100):
573 574
    extra = 0
    orig_dest = dest
575
    while os.path.exists(dest) and extra < too_many:
576 577
        dest = orig_dest + '.' + repr(extra)
        extra += 1
578
    if extra >= too_many:
579 580
        raise tried_too_hard_exc
    return dest
J
James Troup 已提交
581

582
################################################################################
583 584

def result_join (original, sep = '\t'):
585
    list = []
586 587
    for i in xrange(len(original)):
        if original[i] == None:
588
            list.append("")
589
        else:
590 591
            list.append(original[i])
    return sep.join(list)
592

593 594
################################################################################

595
def prefix_multi_line_string(str, prefix, include_blank_lines=0):
596
    out = ""
597
    for line in str.split('\n'):
598
        line = line.strip()
599
        if line or include_blank_lines:
600
            out += "%s%s\n" % (prefix, line)
601 602
    # Strip trailing new line
    if out:
603 604
        out = out[:-1]
    return out
605

606
################################################################################
607

608
def validate_changes_file_arg(filename, require_changes=1):
609 610
    """'filename' is either a .changes or .dak file.  If 'filename' is a
.dak file, it's changed to be the corresponding .changes file.  The
611 612 613 614 615 616 617 618 619 620
function then checks if the .changes file a) exists and b) is
readable and returns the .changes filename if so.  If there's a
problem, the next action depends on the option 'require_changes'
argument:

 o If 'require_changes' == -1, errors are ignored and the .changes
                               filename is returned.
 o If 'require_changes' == 0, a warning is given and 'None' is returned.
 o If 'require_changes' == 1, a fatal error is raised.
"""
621
    error = None
622

623
    orig_filename = filename
624
    if filename.endswith(".dak"):
625
        filename = filename[:-4]+".changes"
626

627
    if not filename.endswith(".changes"):
628
        error = "invalid file type; not a changes file"
629
    else:
630 631
        if not os.access(filename,os.R_OK):
            if os.path.exists(filename):
632
                error = "permission denied"
633
            else:
634
                error = "file not found"
635 636

    if error:
637
        if require_changes == 1:
638
            fubar("%s: %s." % (orig_filename, error))
639
        elif require_changes == 0:
640 641
            warn("Skipping %s - %s" % (orig_filename, error))
            return None
642
        else: # We only care about the .dak file
643
            return filename
644
    else:
645
        return filename
646 647 648

################################################################################

649
def real_arch(arch):
650
    return (arch != "source" and arch != "all")
651 652 653

################################################################################

654
def join_with_commas_and(list):
655 656 657
	if len(list) == 0: return "nothing"
	if len(list) == 1: return list[0]
	return ", ".join(list[:-1]) + " and " + list[-1]
658 659 660

################################################################################

661
def pp_deps (deps):
662
    pp_deps = []
663
    for atom in deps:
664
        (pkg, version, constraint) = atom
665
        if constraint:
666
            pp_dep = "%s (%s %s)" % (pkg, constraint, version)
667
        else:
668 669 670
            pp_dep = pkg
        pp_deps.append(pp_dep)
    return " |".join(pp_deps)
671 672 673

################################################################################

674
def get_conf():
675
	return Cnf
676 677 678

################################################################################

679 680 681 682
# Handle -a, -c and -s arguments; returns them as SQL constraints
def parse_args(Options):
    # Process suite
    if Options["Suite"]:
683
        suite_ids_list = []
684
        for suite in split_args(Options["Suite"]):
J
James Troup 已提交
685
            suite_id = database.get_suite_id(suite)
686
            if suite_id == -1:
687
                warn("suite '%s' not recognised." % (suite))
688
            else:
689
                suite_ids_list.append(suite_id)
690
        if suite_ids_list:
691
            con_suites = "AND su.id IN (%s)" % ", ".join([ str(i) for i in suite_ids_list ])
692
        else:
693
            fubar("No valid suite given.")
694
    else:
695
        con_suites = ""
696 697 698

    # Process component
    if Options["Component"]:
699
        component_ids_list = []
700
        for component in split_args(Options["Component"]):
J
James Troup 已提交
701
            component_id = database.get_component_id(component)
702
            if component_id == -1:
703
                warn("component '%s' not recognised." % (component))
704
            else:
705
                component_ids_list.append(component_id)
706
        if component_ids_list:
707
            con_components = "AND c.id IN (%s)" % ", ".join([ str(i) for i in component_ids_list ])
708
        else:
709
            fubar("No valid component given.")
710
    else:
711
        con_components = ""
712 713

    # Process architecture
714
    con_architectures = ""
715
    if Options["Architecture"]:
716 717
        arch_ids_list = []
        check_source = 0
718
        for architecture in split_args(Options["Architecture"]):
719
            if architecture == "source":
720
                check_source = 1
721
            else:
J
James Troup 已提交
722
                architecture_id = database.get_architecture_id(architecture)
723
                if architecture_id == -1:
724
                    warn("architecture '%s' not recognised." % (architecture))
725
                else:
726
                    arch_ids_list.append(architecture_id)
727
        if arch_ids_list:
728
            con_architectures = "AND a.id IN (%s)" % ", ".join([ str(i) for i in arch_ids_list ])
729 730
        else:
            if not check_source:
731
                fubar("No valid architecture given.")
732
    else:
733
        check_source = 1
734

735
    return (con_suites, con_architectures, con_components, check_source)
736 737 738

################################################################################

739 740 741 742
# Inspired(tm) by Bryn Keller's print_exc_plus (See
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52215)

def print_exc():
743
    tb = sys.exc_info()[2]
744
    while tb.tb_next:
745 746 747
        tb = tb.tb_next
    stack = []
    frame = tb.tb_frame
748
    while frame:
749 750 751 752
        stack.append(frame)
        frame = frame.f_back
    stack.reverse()
    traceback.print_exc()
753 754 755
    for frame in stack:
        print "\nFrame %s in %s at line %s" % (frame.f_code.co_name,
                                             frame.f_code.co_filename,
756
                                             frame.f_lineno)
757
        for key, value in frame.f_locals.items():
758
            print "\t%20s = " % key,
759
            try:
760
                print value
761
            except:
762
                print "<unable to print>"
763 764 765

################################################################################

J
James Troup 已提交
766 767
def try_with_debug(function):
    try:
768
        function()
J
James Troup 已提交
769
    except SystemExit:
770
        raise
J
James Troup 已提交
771
    except:
772
        print_exc()
J
James Troup 已提交
773 774 775

################################################################################

J
James Troup 已提交
776 777 778 779 780
# Function for use in sorting lists of architectures.
# Sorts normally except that 'source' dominates all others.

def arch_compare_sw (a, b):
    if a == "source" and b == "source":
781
        return 0
J
James Troup 已提交
782
    elif a == "source":
783
        return -1
J
James Troup 已提交
784
    elif b == "source":
785
        return 1
J
James Troup 已提交
786

787
    return cmp (a, b)
J
James Troup 已提交
788 789 790

################################################################################

791 792
# Split command line arguments which can be separated by either commas
# or whitespace.  If dwim is set, it will complain about string ending
793
# in comma since this usually means someone did 'dak ls -a i386, m68k
794 795 796 797 798
# 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:
799
        return s.split()
800 801
    else:
        if s[-1:] == "," and dwim:
802 803
            fubar("split_args: found trailing comma, spurious space maybe?")
        return s.split(",")
804 805 806

################################################################################

807 808 809 810 811 812 813
def Dict(**dict): return dict

########################################

# Our very own version of commands.getouputstatus(), hacked to support
# gpgv's status fd.
def gpgv_get_status_output(cmd, status_read, status_write):
814 815 816 817 818
    cmd = ['/bin/sh', '-c', cmd]
    p2cread, p2cwrite = os.pipe()
    c2pread, c2pwrite = os.pipe()
    errout, errin = os.pipe()
    pid = os.fork()
819 820
    if pid == 0:
        # Child
821 822 823 824 825 826
        os.close(0)
        os.close(1)
        os.dup(p2cread)
        os.dup(c2pwrite)
        os.close(2)
        os.dup(errin)
827 828 829
        for i in range(3, 256):
            if i != status_write:
                try:
830
                    os.close(i)
831
                except:
832
                    pass
833
        try:
834
            os.execvp(cmd[0], cmd)
835
        finally:
836
            os._exit(1)
837

838
    # Parent
839
    os.close(p2cread)
840 841
    os.dup2(c2pread, c2pwrite)
    os.dup2(errout, errin)
842

843
    output = status = ""
844
    while 1:
845 846
        i, o, e = select.select([c2pwrite, errin, status_read], [], [])
        more_data = []
847
        for fd in i:
848
            r = os.read(fd, 8196)
849
            if len(r) > 0:
850
                more_data.append(fd)
851
                if fd == c2pwrite or fd == errin:
852
                    output += r
853
                elif fd == status_read:
854
                    status += r
855
                else:
856
                    fubar("Unexpected file descriptor [%s] returned from select\n" % (fd))
857 858 859
        if not more_data:
            pid, exit_status = os.waitpid(pid, 0)
            try:
860 861 862 863 864 865 866
                os.close(status_write)
                os.close(status_read)
                os.close(c2pread)
                os.close(c2pwrite)
                os.close(p2cwrite)
                os.close(errin)
                os.close(errout)
867
            except:
868 869
                pass
            break
870

871
    return output, status, exit_status
872

873
################################################################################
874

875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891
def process_gpgv_output(status):
    # Process the status-fd output
    keywords = {}
    internal_error = ""
    for line in status.split('\n'):
        line = line.strip()
        if line == "":
            continue
        split = line.split()
        if len(split) < 2:
            internal_error += "gpgv status line is malformed (< 2 atoms) ['%s'].\n" % (line)
            continue
        (gnupg, keyword) = split[:2]
        if gnupg != "[GNUPG:]":
            internal_error += "gpgv status line is malformed (incorrect prefix '%s').\n" % (gnupg)
            continue
        args = split[2:]
A
Anthony Towns 已提交
892
        if keywords.has_key(keyword) and keyword not in [ "NODATA", "SIGEXPIRED", "KEYEXPIRED" ]:
893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910
            internal_error += "found duplicate status token ('%s').\n" % (keyword)
            continue
        else:
            keywords[keyword] = args

    return (keywords, internal_error)

################################################################################

def retrieve_key (filename, keyserver=None, keyring=None):
    """Retrieve the key that signed 'filename' from 'keyserver' and
add it to 'keyring'.  Returns nothing on success, or an error message
on error."""

    # Defaults for keyserver and keyring
    if not keyserver:
        keyserver = Cnf["Dinstall::KeyServer"]
    if not keyring:
911
        keyring = Cnf.ValueList("Dinstall::GPGKeyring")[0]
912 913 914 915 916 917 918 919 920

    # Ensure the filename contains no shell meta-characters or other badness
    if not re_taint_free.match(filename):
        return "%s: tainted filename" % (filename)

    # Invoke gpgv on the file
    status_read, status_write = os.pipe(); 
    cmd = "gpgv --status-fd %s --keyring /dev/null %s" % (status_write, filename)
    (_, status, _) = gpgv_get_status_output(cmd, status_read, status_write)
921

922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945
    # Process the status-fd output
    (keywords, internal_error) = process_gpgv_output(status)
    if internal_error:
        return internal_error

    if not keywords.has_key("NO_PUBKEY"):
        return "didn't find expected NO_PUBKEY in gpgv status-fd output"

    fingerprint = keywords["NO_PUBKEY"][0]
    # XXX - gpg sucks.  You can't use --secret-keyring=/dev/null as
    # it'll try to create a lockfile in /dev.  A better solution might
    # be a tempfile or something.
    cmd = "gpg --no-default-keyring --secret-keyring=%s --no-options" \
          % (Cnf["Dinstall::SigningKeyring"])
    cmd += " --keyring %s --keyserver %s --recv-key %s" \
           % (keyring, keyserver, fingerprint)
    (result, output) = commands.getstatusoutput(cmd)
    if (result != 0):
        return "'%s' failed with exit code %s" % (cmd, result)

    return ""

################################################################################

946
def gpg_keyring_args(keyrings=None):
947 948 949 950 951 952 953
    if not keyrings:
        keyrings = Cnf.ValueList("Dinstall::GPGKeyring")

    return " ".join(["--keyring %s" % x for x in keyrings])

################################################################################

954
def check_signature (sig_filename, reject, data_filename="", keyrings=None, autofetch=None):
955 956 957 958 959
    """Check the signature of a file and return the fingerprint if the
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,
960
the second is an optional prefix string.  It's possible for reject()
961 962 963
to be called more than once during an invocation of check_signature().
The third argument is optional and is the name of the files the
detached signature applies to.  The fourth argument is optional and is
964 965 966
a *list* of keyrings to use.  'autofetch' can either be None, True or
False.  If None, the default behaviour specified in the config will be
used."""
967 968

    # Ensure the filename contains no shell meta-characters or other badness
969
    if not re_taint_free.match(sig_filename):
970 971
        reject("!!WARNING!! tainted signature filename: '%s'." % (sig_filename))
        return None
972 973

    if data_filename and not re_taint_free.match(data_filename):
974 975
        reject("!!WARNING!! tainted data filename: '%s'." % (data_filename))
        return None
976 977

    if not keyrings:
978
        keyrings = Cnf.ValueList("Dinstall::GPGKeyring")
979

980 981 982 983 984 985 986 987 988
    # Autofetch the signing key if that's enabled
    if autofetch == None:
        autofetch = Cnf.get("Dinstall::KeyAutoFetch")
    if autofetch:
        error_msg = retrieve_key(sig_filename)
        if error_msg:
            reject(error_msg)
            return None

989 990
    # Build the command line
    status_read, status_write = os.pipe(); 
991 992 993
    cmd = "gpgv --status-fd %s %s %s %s" % (
        status_write, gpg_keyring_args(keyrings), sig_filename, data_filename)

994
    # Invoke gpgv on the file
995
    (output, status, exit_status) = gpgv_get_status_output(cmd, status_read, status_write)
996 997

    # Process the status-fd output
998
    (keywords, internal_error) = process_gpgv_output(status)
999 1000 1001

    # If we failed to parse the status-fd output, let's just whine and bail now
    if internal_error:
1002 1003 1004 1005
        reject("internal error while performing signature check on %s." % (sig_filename))
        reject(internal_error, "")
        reject("Please report the above errors to the Archive maintainers by replying to this mail.", "")
        return None
1006

1007
    bad = ""
1008 1009
    # Now check for obviously bad things in the processed output
    if keywords.has_key("KEYREVOKED"):
1010 1011
        reject("The key used to sign %s has been revoked." % (sig_filename))
        bad = 1
1012
    if keywords.has_key("BADSIG"):
1013 1014
        reject("bad signature on %s." % (sig_filename))
        bad = 1
1015
    if keywords.has_key("ERRSIG") and not keywords.has_key("NO_PUBKEY"):
1016 1017
        reject("failed to check signature on %s." % (sig_filename))
        bad = 1
1018
    if keywords.has_key("NO_PUBKEY"):
1019
        args = keywords["NO_PUBKEY"]
1020
        if len(args) >= 1:
1021 1022 1023
            key = args[0]
        reject("The key (0x%s) used to sign %s wasn't found in the keyring(s)." % (key, sig_filename))
        bad = 1
1024
    if keywords.has_key("BADARMOR"):
1025 1026
        reject("ASCII armour of signature was corrupt in %s." % (sig_filename))
        bad = 1
1027
    if keywords.has_key("NODATA"):
1028 1029
        reject("no signature found in %s." % (sig_filename))
        bad = 1
A
Anthony Towns 已提交
1030
    if keywords.has_key("KEYEXPIRED") and not keywords.has_key("GOODSIG"):
1031 1032 1033
        args = keywords["KEYEXPIRED"]
        if len(args) >= 1:
            key = args[0]
A
Anthony Towns 已提交
1034 1035
        reject("The key (0x%s) used to sign %s has expired." % (key, sig_filename))
        bad = 1
1036 1037

    if bad:
1038
        return None
1039 1040 1041

    # Next check gpgv exited with a zero return code
    if exit_status:
1042
        reject("gpgv failed while checking %s." % (sig_filename))
1043
        if status.strip():
1044
            reject(prefix_multi_line_string(status, " [GPG status-fd output:] "), "")
1045
        else:
1046 1047
            reject(prefix_multi_line_string(output, " [GPG output:] "), "")
        return None
1048 1049 1050

    # Sanity check the good stuff we expect
    if not keywords.has_key("VALIDSIG"):
1051 1052
        reject("signature on %s does not appear to be valid [No VALIDSIG]." % (sig_filename))
        bad = 1
1053
    else:
1054
        args = keywords["VALIDSIG"]
1055
        if len(args) < 1:
1056 1057
            reject("internal error while checking signature on %s." % (sig_filename))
            bad = 1
1058
        else:
1059
            fingerprint = args[0]
1060
    if not keywords.has_key("GOODSIG"):
1061 1062
        reject("signature on %s does not appear to be valid [No GOODSIG]." % (sig_filename))
        bad = 1
1063
    if not keywords.has_key("SIG_ID"):
1064 1065
        reject("signature on %s does not appear to be valid [No SIG_ID]." % (sig_filename))
        bad = 1
1066 1067 1068 1069

    # Finally ensure there's not something we don't recognise
    known_keywords = Dict(VALIDSIG="",SIG_ID="",GOODSIG="",BADSIG="",ERRSIG="",
                          SIGEXPIRED="",KEYREVOKED="",NO_PUBKEY="",BADARMOR="",
1070
                          NODATA="",NOTATION_DATA="",NOTATION_NAME="",KEYEXPIRED="")
1071 1072 1073

    for keyword in keywords.keys():
        if not known_keywords.has_key(keyword):
1074 1075
            reject("found unknown status token '%s' from gpgv with args '%r' in %s." % (keyword, keywords[keyword], sig_filename))
            bad = 1
1076 1077

    if bad:
1078
        return None
1079
    else:
1080
        return fingerprint
1081 1082 1083

################################################################################

1084 1085 1086
# Inspired(tm) by http://www.zopelabs.com/cookbook/1022242603

def wrap(paragraph, max_length, prefix=""):
1087 1088 1089 1090
    line = ""
    s = ""
    have_started = 0
    words = paragraph.split()
1091 1092

    for word in words:
1093
        word_size = len(word)
1094 1095
        if word_size > max_length:
            if have_started:
1096 1097
                s += line + '\n' + prefix
            s += word + '\n' + prefix
1098 1099
        else:
            if have_started:
1100
                new_length = len(line) + word_size + 1
1101
                if new_length > max_length:
1102 1103
                    s += line + '\n' + prefix
                    line = word
1104
                else:
1105
                    line += ' ' + word
1106
            else:
1107 1108
                line = word
        have_started = 1
1109 1110

    if have_started:
1111
        s += line
1112

1113
    return s
1114 1115 1116 1117 1118 1119

################################################################################

# Relativize an absolute symlink from 'src' -> 'dest' relative to 'root'.
# Returns fixed 'src'
def clean_symlink (src, dest, root):
1120 1121 1122 1123 1124
    src = src.replace(root, '', 1)
    dest = dest.replace(root, '', 1)
    dest = os.path.dirname(dest)
    new_src = '../' * len(dest.split('/'))
    return new_src + src
1125 1126 1127

################################################################################

1128 1129 1130 1131 1132 1133
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:
1134 1135
        old_tempdir = tempfile.tempdir
        tempfile.tempdir = directory
1136

1137
    filename = tempfile.mktemp()
1138 1139

    if dotprefix:
1140 1141 1142
        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)
1143 1144

    if directory:
1145
        tempfile.tempdir = old_tempdir
1146

1147
    return filename
1148 1149 1150

################################################################################

1151
apt_pkg.init()
1152

1153 1154
Cnf = apt_pkg.newConfiguration()
apt_pkg.ReadConfigFileISC(Cnf,default_config)
1155 1156

if which_conf_file() != default_config:
1157
	apt_pkg.ReadConfigFileISC(Cnf,which_conf_file())
1158 1159

################################################################################