utils.py 37.3 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 46
re_srchasver = re.compile(r"^(\S+)\s+\((\S+)\)$")

47 48 49 50 51 52
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 已提交
53
cant_overwrite_exc = "Permission denied; can't overwrite existent file."
54 55 56
file_exists_exc = "Destination file exists"
sendmail_failed_exc = "Sendmail invocation failed"
tried_too_hard_exc = "Tried too hard to find a free filename."
57

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

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

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

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

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

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

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

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

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

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

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

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

105
    if section.find('/') != -1:
106
        component = section.split('/')[0]
107
    if component.lower() == "non-us" and section.find('/') != -1:
108
        s = component + '/' + section.split('/')[1]
109
        if Cnf.has_key("Component::%s" % s): # Avoid e.g. non-US/libs
110
            component = s
J
James Troup 已提交
111

112
    if section.lower() == "non-us":
113
        component = "non-US/main"
114 115

    # non-US prefix is case insensitive
116
    if component.lower()[:6] == "non-us":
117
        component = "non-US"+component[6:]
118 119

    # Expand default component
J
James Troup 已提交
120
    if component == "":
121
        if Cnf.has_key("Component::%s" % section):
122
            component = section
123
        else:
124
            component = "main"
125
    elif component == "non-US":
126
        component = "non-US/main"
J
James Troup 已提交
127

128
    return (section, component)
J
James Troup 已提交
129

130 131
################################################################################

132 133 134 135
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 已提交
136

137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
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 已提交
152

153 154
    error = ""
    changes = {}
155

156 157
    changes_in = open_file(filename)
    lines = changes_in.readlines()
J
James Troup 已提交
158

159
    if not lines:
160
	raise changes_parse_error_exc, "[Empty changes file]"
161

J
James Troup 已提交
162 163
    # Reindex by line number so we can easily verify the format of
    # .dsc files...
164 165
    index = 0
    indexed_lines = {}
J
James Troup 已提交
166
    for line in lines:
167 168
        index += 1
        indexed_lines[index] = line[:-1]
J
James Troup 已提交
169

170
    inside_signature = 0
J
James Troup 已提交
171

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

222
    if signing_rules == 1 and inside_signature:
223
        raise invalid_dsc_format_exc, index
J
James Troup 已提交
224

225 226
    changes_in.close()
    changes["filecontents"] = "".join(lines)
J
James Troup 已提交
227

228 229 230 231 232 233 234 235
    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)

236
    if error:
237
	raise changes_parse_error_exc, error
J
James Troup 已提交
238

239
    return changes
J
James Troup 已提交
240

241
################################################################################
J
James Troup 已提交
242 243 244

# Dropped support for 1.4 and ``buggy dchanges 3.4'' (?!) compared to di.pl

245
def build_file_list(changes, is_a_dsc=0):
246
    files = {}
247 248 249

    # Make sure we have a Files: field to parse...
    if not changes.has_key("files"):
250
	raise no_files_exc
251 252

    # Make sure we recognise the format of the Files: field
253
    format = changes.get("format", "")
J
James Troup 已提交
254
    if format != "":
255
	format = float(format)
256
    if not is_a_dsc and (format < 1.5 or format > 2.0):
257
	raise nk_format_exc, format
J
James Troup 已提交
258

259 260 261
    # Parse each entry/line:
    for i in changes["files"].split('\n'):
        if not i:
262 263 264
            break
        s = i.split()
        section = priority = ""
J
sync  
James Troup 已提交
265
        try:
266
            if is_a_dsc:
267
                (md5, size, name) = s
J
sync  
James Troup 已提交
268
            else:
269
                (md5, size, section, priority, name) = s
J
sync  
James Troup 已提交
270
        except ValueError:
271
            raise changes_parse_error_exc, i
J
James Troup 已提交
272

273
        if section == "":
274
            section = "-"
275
        if priority == "":
276
            priority = "-"
J
James Troup 已提交
277

278
        (section, component) = extract_component_from_section(section)
J
James Troup 已提交
279

280
        files[name] = Dict(md5sum=md5, size=size, section=section,
281
                           priority=priority, component=component)
J
James Troup 已提交
282 283 284

    return files

285
################################################################################
J
James Troup 已提交
286

287 288 289 290
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:
291 292
        unicode(s, 'utf-8')
        return s
293
    except UnicodeError:
294 295
        latin1_s = unicode(s,'iso8859-1')
        return latin1_s.encode('utf-8')
296 297 298 299 300

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:
301
        codecs.lookup('ascii')[1](s)
302
        return s
303
    except UnicodeError:
304
        pass
305
    try:
306
        codecs.lookup('utf-8')[1](s)
307 308
        h = email.Header.Header(s, 'utf-8', 998)
        return str(h)
309
    except UnicodeError:
310 311
        h = email.Header.Header(s, 'iso-8859-1', 998)
        return str(h)
312 313 314 315 316 317

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

# <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 已提交
318

J
James Troup 已提交
319
def fix_maintainer (maintainer):
320 321 322 323 324 325 326 327 328 329 330
    """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:
331
        return ('', '', '', '')
332

333
    if maintainer.find("<") == -1:
334 335
        email = maintainer
        name = ""
336
    elif (maintainer[0] == "<" and maintainer[-1:] == ">"):
337 338
        email = maintainer[1:-1]
        name = ""
339
    else:
340
        m = re_parse_maintainer.match(maintainer)
341 342
        if not m:
            raise ParseMaintError, "Doesn't parse as a valid Maintainer field."
343 344
        name = m.group(1)
        email = m.group(2)
345 346

    # Get an RFC2047 compliant version of the name
347
    rfc2047_name = rfc2047_encode(name)
348 349

    # Force the name to be UTF-8
350
    name = force_to_utf8(name)
351 352

    if name.find(',') != -1 or name.find('.') != -1:
353 354
        rfc822_maint = "%s (%s)" % (email, name)
        rfc2047_maint = "%s (%s)" % (email, rfc2047_name)
355
    else:
356 357
        rfc822_maint = "%s <%s>" % (name, email)
        rfc2047_maint = "%s <%s>" % (rfc2047_name, email)
358 359 360 361

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

362
    return (rfc822_maint, rfc2047_maint, name, email)
J
James Troup 已提交
363

364
################################################################################
J
James Troup 已提交
365 366

# sendmail wrapper, takes _either_ a message string or a file as arguments
367
def send_mail (message, filename=""):
J
James Troup 已提交
368
	# If we've been passed a string dump it into a temporary file
369
	if message:
370 371 372 373
            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 已提交
374

J
James Troup 已提交
375
	# Invoke sendmail
376
	(result, output) = commands.getstatusoutput("%s < %s" % (Cnf["Dinstall::SendmailCommand"], filename))
J
James Troup 已提交
377
	if (result != 0):
378
            raise sendmail_failed_exc, output
J
James Troup 已提交
379

J
James Troup 已提交
380
	# Clean up any temporary files
381
	if message:
382
            os.unlink (filename)
J
James Troup 已提交
383

384
################################################################################
J
James Troup 已提交
385 386

def poolify (source, component):
387
    if component:
388
	component += '/'
J
James Troup 已提交
389
    # FIXME: this is nasty
390
    component = component.lower().replace("non-us/", "non-US/")
J
James Troup 已提交
391 392 393 394 395
    if source[:3] == "lib":
	return component + source[:4] + '/' + source + '/'
    else:
	return component + source[:1] + '/' + source + '/'

396
################################################################################
J
James Troup 已提交
397

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

421
def copy (src, dest, overwrite = 0, perms = 0664):
J
James Troup 已提交
422
    if os.path.exists(dest) and os.path.isdir(dest):
423
	dest_dir = dest
J
James Troup 已提交
424
    else:
425
	dest_dir = os.path.dirname(dest)
J
James Troup 已提交
426
    if not os.path.exists(dest_dir):
427 428 429 430
	umask = os.umask(00000)
	os.makedirs(dest_dir, 02775)
	os.umask(umask)
    #print "Copying %s to %s..." % (src, dest)
J
James Troup 已提交
431
    if os.path.exists(dest) and os.path.isdir(dest):
432
	dest += '/' + os.path.basename(src)
433 434 435 436 437 438 439
    # 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
440 441
    shutil.copy2(src, dest)
    os.chmod(dest, perms)
J
James Troup 已提交
442

443
################################################################################
J
James Troup 已提交
444 445

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

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

def which_apt_conf_file ():
461
    res = socket.gethostbyaddr(socket.gethostname())
462 463
    if Cnf.get("Config::" + res[0] + "::AptConfig"):
	return Cnf["Config::" + res[0] + "::AptConfig"]
464
    else:
465
	return default_apt_config
466

467
################################################################################
J
James Troup 已提交
468

469 470 471 472
# Escape characters which have meaning to SQL's regex comparison operator ('~')
# (woefully incomplete)

def regex_safe (s):
473 474
    s = s.replace('+', '\\\\+')
    s = s.replace('.', '\\\\.')
475 476
    return s

477
################################################################################
478

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

488
################################################################################
J
James Troup 已提交
489 490

def fubar(msg, exit_code=1):
491 492
    sys.stderr.write("E: %s\n" % (msg))
    sys.exit(exit_code)
J
James Troup 已提交
493 494

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

497
################################################################################
J
James Troup 已提交
498

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

504
################################################################################
J
James Troup 已提交
505

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

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

def cc_fix_changes (changes):
519
    o = changes.get("architecture", "")
520
    if o:
521 522
        del changes["architecture"]
    changes["architecture"] = {}
523
    for j in o.split():
524
        changes["architecture"][j] = 1
525

526
# Sort by source name, source version, 'have source', and then by filename
527 528
def changes_compare (a, b):
    try:
529
        a_changes = parse_changes(a)
530
    except:
531
        return -1
532 533

    try:
534
        b_changes = parse_changes(b)
535
    except:
536
        return 1
J
James Troup 已提交
537

538 539
    cc_fix_changes (a_changes)
    cc_fix_changes (b_changes)
540 541

    # Sort by source name
542 543 544
    a_source = a_changes.get("source")
    b_source = b_changes.get("source")
    q = cmp (a_source, b_source)
545
    if q:
546
        return q
547 548

    # Sort by source version
549 550 551
    a_version = a_changes.get("version", "0")
    b_version = b_changes.get("version", "0")
    q = apt_pkg.VersionCompare(a_version, b_version)
552
    if q:
553
        return q
554

555
    # Sort by 'have source'
556 557
    a_has_source = a_changes["architecture"].get("source")
    b_has_source = b_changes["architecture"].get("source")
558
    if a_has_source and not b_has_source:
559
        return -1
560
    elif b_has_source and not a_has_source:
561
        return 1
562

563
    # Fall back to sort by filename
564
    return cmp(a, b)
565 566

################################################################################
567 568

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

578
################################################################################
579 580

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

589 590
################################################################################

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

602
################################################################################
603

604
def validate_changes_file_arg(filename, require_changes=1):
605 606
    """'filename' is either a .changes or .dak file.  If 'filename' is a
.dak file, it's changed to be the corresponding .changes file.  The
607 608 609 610 611 612 613 614 615 616
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.
"""
617
    error = None
618

619
    orig_filename = filename
620
    if filename.endswith(".dak"):
621
        filename = filename[:-4]+".changes"
622

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

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

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

645
def real_arch(arch):
646
    return (arch != "source" and arch != "all")
647 648 649

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

650
def join_with_commas_and(list):
651 652 653
	if len(list) == 0: return "nothing"
	if len(list) == 1: return list[0]
	return ", ".join(list[:-1]) + " and " + list[-1]
654 655 656

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

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

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

670
def get_conf():
671
	return Cnf
672 673 674

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

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

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

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

731
    return (con_suites, con_architectures, con_components, check_source)
732 733 734

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

735 736 737 738
# Inspired(tm) by Bryn Keller's print_exc_plus (See
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52215)

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

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

J
James Troup 已提交
762 763
def try_with_debug(function):
    try:
764
        function()
J
James Troup 已提交
765
    except SystemExit:
766
        raise
J
James Troup 已提交
767
    except:
768
        print_exc()
J
James Troup 已提交
769 770 771

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

J
James Troup 已提交
772 773 774 775 776
# 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":
777
        return 0
J
James Troup 已提交
778
    elif a == "source":
779
        return -1
J
James Troup 已提交
780
    elif b == "source":
781
        return 1
J
James Troup 已提交
782

783
    return cmp (a, b)
J
James Troup 已提交
784 785 786

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

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

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

803 804 805 806 807 808 809
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):
810 811 812 813 814
    cmd = ['/bin/sh', '-c', cmd]
    p2cread, p2cwrite = os.pipe()
    c2pread, c2pwrite = os.pipe()
    errout, errin = os.pipe()
    pid = os.fork()
815 816
    if pid == 0:
        # Child
817 818 819 820 821 822
        os.close(0)
        os.close(1)
        os.dup(p2cread)
        os.dup(c2pwrite)
        os.close(2)
        os.dup(errin)
823 824 825
        for i in range(3, 256):
            if i != status_write:
                try:
826
                    os.close(i)
827
                except:
828
                    pass
829
        try:
830
            os.execvp(cmd[0], cmd)
831
        finally:
832
            os._exit(1)
833

834
    # Parent
835
    os.close(p2cread)
836 837
    os.dup2(c2pread, c2pwrite)
    os.dup2(errout, errin)
838

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

867
    return output, status, exit_status
868

869
################################################################################
870

871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906
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:]
        if keywords.has_key(keyword) and (keyword != "NODATA" and keyword != "SIGEXPIRED"):
            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:
907
        keyring = Cnf.ValueList("Dinstall::GPGKeyring")[0]
908 909 910 911 912 913 914 915 916

    # 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)
917

918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941
    # 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 ""

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

942
def gpg_keyring_args(keyrings=None):
943 944 945 946 947 948 949
    if not keyrings:
        keyrings = Cnf.ValueList("Dinstall::GPGKeyring")

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

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

950
def check_signature (sig_filename, reject, data_filename="", keyrings=None, autofetch=None):
951 952 953 954 955
    """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,
956
the second is an optional prefix string.  It's possible for reject()
957 958 959
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
960 961 962
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."""
963 964

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

    if data_filename and not re_taint_free.match(data_filename):
970 971
        reject("!!WARNING!! tainted data filename: '%s'." % (data_filename))
        return None
972 973

    if not keyrings:
974
        keyrings = Cnf.ValueList("Dinstall::GPGKeyring")
975

976 977 978 979 980 981 982 983 984
    # 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

985 986
    # Build the command line
    status_read, status_write = os.pipe(); 
987 988 989
    cmd = "gpgv --status-fd %s %s %s %s" % (
        status_write, gpg_keyring_args(keyrings), sig_filename, data_filename)

990
    # Invoke gpgv on the file
991
    (output, status, exit_status) = gpgv_get_status_output(cmd, status_read, status_write)
992 993

    # Process the status-fd output
994
    (keywords, internal_error) = process_gpgv_output(status)
995 996 997

    # If we failed to parse the status-fd output, let's just whine and bail now
    if internal_error:
998 999 1000 1001
        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
1002

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

    if bad:
1031
        return None
1032 1033 1034

    # Next check gpgv exited with a zero return code
    if exit_status:
1035
        reject("gpgv failed while checking %s." % (sig_filename))
1036
        if status.strip():
1037
            reject(prefix_multi_line_string(status, " [GPG status-fd output:] "), "")
1038
        else:
1039 1040
            reject(prefix_multi_line_string(output, " [GPG output:] "), "")
        return None
1041 1042 1043

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

    # Finally ensure there's not something we don't recognise
    known_keywords = Dict(VALIDSIG="",SIG_ID="",GOODSIG="",BADSIG="",ERRSIG="",
                          SIGEXPIRED="",KEYREVOKED="",NO_PUBKEY="",BADARMOR="",
1063
                          NODATA="")
1064 1065 1066

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

    if bad:
1071
        return None
1072
    else:
1073
        return fingerprint
1074 1075 1076

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

1077 1078 1079
# Inspired(tm) by http://www.zopelabs.com/cookbook/1022242603

def wrap(paragraph, max_length, prefix=""):
1080 1081 1082 1083
    line = ""
    s = ""
    have_started = 0
    words = paragraph.split()
1084 1085

    for word in words:
1086
        word_size = len(word)
1087 1088
        if word_size > max_length:
            if have_started:
1089 1090
                s += line + '\n' + prefix
            s += word + '\n' + prefix
1091 1092
        else:
            if have_started:
1093
                new_length = len(line) + word_size + 1
1094
                if new_length > max_length:
1095 1096
                    s += line + '\n' + prefix
                    line = word
1097
                else:
1098
                    line += ' ' + word
1099
            else:
1100 1101
                line = word
        have_started = 1
1102 1103

    if have_started:
1104
        s += line
1105

1106
    return s
1107 1108 1109 1110 1111 1112

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

# Relativize an absolute symlink from 'src' -> 'dest' relative to 'root'.
# Returns fixed 'src'
def clean_symlink (src, dest, root):
1113 1114 1115 1116 1117
    src = src.replace(root, '', 1)
    dest = dest.replace(root, '', 1)
    dest = os.path.dirname(dest)
    new_src = '../' * len(dest.split('/'))
    return new_src + src
1118 1119 1120

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

1121 1122 1123 1124 1125 1126
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:
1127 1128
        old_tempdir = tempfile.tempdir
        tempfile.tempdir = directory
1129

1130
    filename = tempfile.mktemp()
1131 1132

    if dotprefix:
1133 1134 1135
        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)
1136 1137

    if directory:
1138
        tempfile.tempdir = old_tempdir
1139

1140
    return filename
1141 1142 1143

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

1144
apt_pkg.init()
1145

1146 1147
Cnf = apt_pkg.newConfiguration()
apt_pkg.ReadConfigFileISC(Cnf,default_config)
1148 1149

if which_conf_file() != default_config:
1150
	apt_pkg.ReadConfigFileISC(Cnf,which_conf_file())
1151 1152

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