utils.py 36.9 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 108

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

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

117 118
################################################################################

119 120 121 122
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 已提交
123

124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
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 已提交
139

140 141
    error = ""
    changes = {}
142

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

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

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

157
    inside_signature = 0
J
James Troup 已提交
158

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

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

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

215 216 217 218 219 220 221 222
    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)

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

226
    return changes
J
James Troup 已提交
227

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

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

232
def build_file_list(changes, is_a_dsc=0):
233
    files = {}
234 235 236

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

    # Make sure we recognise the format of the Files: field
240
    format = changes.get("format", "")
J
James Troup 已提交
241
    if format != "":
242
	format = float(format)
J
Joerg Jaspert 已提交
243
    if not is_a_dsc and (format < 1.5 or format > 1.7):
244
	raise nk_format_exc, format
J
James Troup 已提交
245

246 247 248
    # Parse each entry/line:
    for i in changes["files"].split('\n'):
        if not i:
249 250 251
            break
        s = i.split()
        section = priority = ""
J
sync  
James Troup 已提交
252
        try:
253
            if is_a_dsc:
254
                (md5, size, name) = s
J
sync  
James Troup 已提交
255
            else:
256
                (md5, size, section, priority, name) = s
J
sync  
James Troup 已提交
257
        except ValueError:
258
            raise changes_parse_error_exc, i
J
James Troup 已提交
259

260
        if section == "":
261
            section = "-"
262
        if priority == "":
263
            priority = "-"
J
James Troup 已提交
264

265
        (section, component) = extract_component_from_section(section)
J
James Troup 已提交
266

267
        files[name] = Dict(md5sum=md5, size=size, section=section,
268
                           priority=priority, component=component)
J
James Troup 已提交
269 270 271

    return files

272
################################################################################
J
James Troup 已提交
273

274 275 276 277
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:
278 279
        unicode(s, 'utf-8')
        return s
280
    except UnicodeError:
281 282
        latin1_s = unicode(s,'iso8859-1')
        return latin1_s.encode('utf-8')
283 284 285 286 287

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:
288
        codecs.lookup('ascii')[1](s)
289
        return s
290
    except UnicodeError:
291
        pass
292
    try:
293
        codecs.lookup('utf-8')[1](s)
294 295
        h = email.Header.Header(s, 'utf-8', 998)
        return str(h)
296
    except UnicodeError:
297 298
        h = email.Header.Header(s, 'iso-8859-1', 998)
        return str(h)
299 300 301 302 303 304

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

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

J
James Troup 已提交
306
def fix_maintainer (maintainer):
307 308 309 310 311 312 313 314 315 316 317
    """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:
318
        return ('', '', '', '')
319

320
    if maintainer.find("<") == -1:
321 322
        email = maintainer
        name = ""
323
    elif (maintainer[0] == "<" and maintainer[-1:] == ">"):
324 325
        email = maintainer[1:-1]
        name = ""
326
    else:
327
        m = re_parse_maintainer.match(maintainer)
328 329
        if not m:
            raise ParseMaintError, "Doesn't parse as a valid Maintainer field."
330 331
        name = m.group(1)
        email = m.group(2)
332 333

    # Get an RFC2047 compliant version of the name
334
    rfc2047_name = rfc2047_encode(name)
335 336

    # Force the name to be UTF-8
337
    name = force_to_utf8(name)
338 339

    if name.find(',') != -1 or name.find('.') != -1:
340 341
        rfc822_maint = "%s (%s)" % (email, name)
        rfc2047_maint = "%s (%s)" % (email, rfc2047_name)
342
    else:
343 344
        rfc822_maint = "%s <%s>" % (name, email)
        rfc2047_maint = "%s <%s>" % (rfc2047_name, email)
345 346 347 348

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

349
    return (rfc822_maint, rfc2047_maint, name, email)
J
James Troup 已提交
350

351
################################################################################
J
James Troup 已提交
352 353

# sendmail wrapper, takes _either_ a message string or a file as arguments
354
def send_mail (message, filename=""):
J
James Troup 已提交
355
	# If we've been passed a string dump it into a temporary file
356
	if message:
357 358 359 360
            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 已提交
361

J
James Troup 已提交
362
	# Invoke sendmail
363
	(result, output) = commands.getstatusoutput("%s < %s" % (Cnf["Dinstall::SendmailCommand"], filename))
J
James Troup 已提交
364
	if (result != 0):
365
            raise sendmail_failed_exc, output
J
James Troup 已提交
366

J
James Troup 已提交
367
	# Clean up any temporary files
368
	if message:
369
            os.unlink (filename)
J
James Troup 已提交
370

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

def poolify (source, component):
374
    if component:
375
	component += '/'
J
James Troup 已提交
376 377 378 379 380
    if source[:3] == "lib":
	return component + source[:4] + '/' + source + '/'
    else:
	return component + source[:1] + '/' + source + '/'

381
################################################################################
J
James Troup 已提交
382

383
def move (src, dest, overwrite = 0, perms = 0664):
J
James Troup 已提交
384
    if os.path.exists(dest) and os.path.isdir(dest):
385
	dest_dir = dest
J
James Troup 已提交
386
    else:
387
	dest_dir = os.path.dirname(dest)
J
James Troup 已提交
388
    if not os.path.exists(dest_dir):
389 390 391 392
	umask = os.umask(00000)
	os.makedirs(dest_dir, 02775)
	os.umask(umask)
    #print "Moving %s to %s..." % (src, dest)
J
James Troup 已提交
393
    if os.path.exists(dest) and os.path.isdir(dest):
394
	dest += '/' + os.path.basename(src)
395 396 397
    # Don't overwrite unless forced to
    if os.path.exists(dest):
        if not overwrite:
398
            fubar("Can't move %s to %s - file already exists." % (src, dest))
399 400
        else:
            if not os.access(dest, os.W_OK):
401 402 403 404
                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 已提交
405

406
def copy (src, dest, overwrite = 0, perms = 0664):
J
James Troup 已提交
407
    if os.path.exists(dest) and os.path.isdir(dest):
408
	dest_dir = dest
J
James Troup 已提交
409
    else:
410
	dest_dir = os.path.dirname(dest)
J
James Troup 已提交
411
    if not os.path.exists(dest_dir):
412 413 414 415
	umask = os.umask(00000)
	os.makedirs(dest_dir, 02775)
	os.umask(umask)
    #print "Copying %s to %s..." % (src, dest)
J
James Troup 已提交
416
    if os.path.exists(dest) and os.path.isdir(dest):
417
	dest += '/' + os.path.basename(src)
418 419 420 421 422 423 424
    # 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
425 426
    shutil.copy2(src, dest)
    os.chmod(dest, perms)
J
James Troup 已提交
427

428
################################################################################
J
James Troup 已提交
429 430

def where_am_i ():
431 432
    res = socket.gethostbyaddr(socket.gethostname())
    database_hostname = Cnf.get("Config::" + res[0] + "::DatabaseHostname")
433
    if database_hostname:
434
	return database_hostname
J
James Troup 已提交
435
    else:
436
        return res[0]
J
James Troup 已提交
437 438

def which_conf_file ():
439
    res = socket.gethostbyaddr(socket.gethostname())
440 441
    if Cnf.get("Config::" + res[0] + "::DakConfig"):
	return Cnf["Config::" + res[0] + "::DakConfig"]
J
James Troup 已提交
442
    else:
443
	return default_config
444 445

def which_apt_conf_file ():
446
    res = socket.gethostbyaddr(socket.gethostname())
447 448
    if Cnf.get("Config::" + res[0] + "::AptConfig"):
	return Cnf["Config::" + res[0] + "::AptConfig"]
449
    else:
450
	return default_apt_config
451

452
################################################################################
J
James Troup 已提交
453

454 455 456 457
# Escape characters which have meaning to SQL's regex comparison operator ('~')
# (woefully incomplete)

def regex_safe (s):
458 459
    s = s.replace('+', '\\\\+')
    s = s.replace('.', '\\\\.')
460 461
    return s

462
################################################################################
463

J
James Troup 已提交
464
# Perform a substition of template
465
def TemplateSubst(map, filename):
466 467
    file = open_file(filename)
    template = file.read()
468
    for x in map.keys():
469 470 471
        template = template.replace(x,map[x])
    file.close()
    return template
J
James Troup 已提交
472

473
################################################################################
J
James Troup 已提交
474 475

def fubar(msg, exit_code=1):
476 477
    sys.stderr.write("E: %s\n" % (msg))
    sys.exit(exit_code)
J
James Troup 已提交
478 479

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

482
################################################################################
J
James Troup 已提交
483

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

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

J
James Troup 已提交
491
def size_type (c):
492
    t  = " B"
493
    if c > 10240:
494 495
        c = c / 1024
        t = " KB"
496
    if c > 10240:
497 498
        c = c / 1024
        t = " MB"
J
James Troup 已提交
499
    return ("%d%s" % (c, t))
500 501 502 503

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

def cc_fix_changes (changes):
504
    o = changes.get("architecture", "")
505
    if o:
506 507
        del changes["architecture"]
    changes["architecture"] = {}
508
    for j in o.split():
509
        changes["architecture"][j] = 1
510

511
# Sort by source name, source version, 'have source', and then by filename
512 513
def changes_compare (a, b):
    try:
514
        a_changes = parse_changes(a)
515
    except:
516
        return -1
517 518

    try:
519
        b_changes = parse_changes(b)
520
    except:
521
        return 1
J
James Troup 已提交
522

523 524
    cc_fix_changes (a_changes)
    cc_fix_changes (b_changes)
525 526

    # Sort by source name
527 528 529
    a_source = a_changes.get("source")
    b_source = b_changes.get("source")
    q = cmp (a_source, b_source)
530
    if q:
531
        return q
532 533

    # Sort by source version
534 535 536
    a_version = a_changes.get("version", "0")
    b_version = b_changes.get("version", "0")
    q = apt_pkg.VersionCompare(a_version, b_version)
537
    if q:
538
        return q
539

540
    # Sort by 'have source'
541 542
    a_has_source = a_changes["architecture"].get("source")
    b_has_source = b_changes["architecture"].get("source")
543
    if a_has_source and not b_has_source:
544
        return -1
545
    elif b_has_source and not a_has_source:
546
        return 1
547

548
    # Fall back to sort by filename
549
    return cmp(a, b)
550 551

################################################################################
552 553

def find_next_free (dest, too_many=100):
554 555
    extra = 0
    orig_dest = dest
556
    while os.path.exists(dest) and extra < too_many:
557 558
        dest = orig_dest + '.' + repr(extra)
        extra += 1
559
    if extra >= too_many:
560 561
        raise tried_too_hard_exc
    return dest
J
James Troup 已提交
562

563
################################################################################
564 565

def result_join (original, sep = '\t'):
566
    list = []
567 568
    for i in xrange(len(original)):
        if original[i] == None:
569
            list.append("")
570
        else:
571 572
            list.append(original[i])
    return sep.join(list)
573

574 575
################################################################################

576
def prefix_multi_line_string(str, prefix, include_blank_lines=0):
577
    out = ""
578
    for line in str.split('\n'):
579
        line = line.strip()
580
        if line or include_blank_lines:
581
            out += "%s%s\n" % (prefix, line)
582 583
    # Strip trailing new line
    if out:
584 585
        out = out[:-1]
    return out
586

587
################################################################################
588

589
def validate_changes_file_arg(filename, require_changes=1):
590 591
    """'filename' is either a .changes or .dak file.  If 'filename' is a
.dak file, it's changed to be the corresponding .changes file.  The
592 593 594 595 596 597 598 599 600 601
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.
"""
602
    error = None
603

604
    orig_filename = filename
605
    if filename.endswith(".dak"):
606
        filename = filename[:-4]+".changes"
607

608
    if not filename.endswith(".changes"):
609
        error = "invalid file type; not a changes file"
610
    else:
611 612
        if not os.access(filename,os.R_OK):
            if os.path.exists(filename):
613
                error = "permission denied"
614
            else:
615
                error = "file not found"
616 617

    if error:
618
        if require_changes == 1:
619
            fubar("%s: %s." % (orig_filename, error))
620
        elif require_changes == 0:
621 622
            warn("Skipping %s - %s" % (orig_filename, error))
            return None
623
        else: # We only care about the .dak file
624
            return filename
625
    else:
626
        return filename
627 628 629

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

630
def real_arch(arch):
631
    return (arch != "source" and arch != "all")
632 633 634

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

635
def join_with_commas_and(list):
636 637 638
	if len(list) == 0: return "nothing"
	if len(list) == 1: return list[0]
	return ", ".join(list[:-1]) + " and " + list[-1]
639 640 641

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

642
def pp_deps (deps):
643
    pp_deps = []
644
    for atom in deps:
645
        (pkg, version, constraint) = atom
646
        if constraint:
647
            pp_dep = "%s (%s %s)" % (pkg, constraint, version)
648
        else:
649 650 651
            pp_dep = pkg
        pp_deps.append(pp_dep)
    return " |".join(pp_deps)
652 653 654

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

655
def get_conf():
656
	return Cnf
657 658 659

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

660 661 662 663
# Handle -a, -c and -s arguments; returns them as SQL constraints
def parse_args(Options):
    # Process suite
    if Options["Suite"]:
664
        suite_ids_list = []
665
        for suite in split_args(Options["Suite"]):
J
James Troup 已提交
666
            suite_id = database.get_suite_id(suite)
667
            if suite_id == -1:
668
                warn("suite '%s' not recognised." % (suite))
669
            else:
670
                suite_ids_list.append(suite_id)
671
        if suite_ids_list:
672
            con_suites = "AND su.id IN (%s)" % ", ".join([ str(i) for i in suite_ids_list ])
673
        else:
674
            fubar("No valid suite given.")
675
    else:
676
        con_suites = ""
677 678 679

    # Process component
    if Options["Component"]:
680
        component_ids_list = []
681
        for component in split_args(Options["Component"]):
J
James Troup 已提交
682
            component_id = database.get_component_id(component)
683
            if component_id == -1:
684
                warn("component '%s' not recognised." % (component))
685
            else:
686
                component_ids_list.append(component_id)
687
        if component_ids_list:
688
            con_components = "AND c.id IN (%s)" % ", ".join([ str(i) for i in component_ids_list ])
689
        else:
690
            fubar("No valid component given.")
691
    else:
692
        con_components = ""
693 694

    # Process architecture
695
    con_architectures = ""
696
    if Options["Architecture"]:
697 698
        arch_ids_list = []
        check_source = 0
699
        for architecture in split_args(Options["Architecture"]):
700
            if architecture == "source":
701
                check_source = 1
702
            else:
J
James Troup 已提交
703
                architecture_id = database.get_architecture_id(architecture)
704
                if architecture_id == -1:
705
                    warn("architecture '%s' not recognised." % (architecture))
706
                else:
707
                    arch_ids_list.append(architecture_id)
708
        if arch_ids_list:
709
            con_architectures = "AND a.id IN (%s)" % ", ".join([ str(i) for i in arch_ids_list ])
710 711
        else:
            if not check_source:
712
                fubar("No valid architecture given.")
713
    else:
714
        check_source = 1
715

716
    return (con_suites, con_architectures, con_components, check_source)
717 718 719

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

720 721 722 723
# Inspired(tm) by Bryn Keller's print_exc_plus (See
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52215)

def print_exc():
724
    tb = sys.exc_info()[2]
725
    while tb.tb_next:
726 727 728
        tb = tb.tb_next
    stack = []
    frame = tb.tb_frame
729
    while frame:
730 731 732 733
        stack.append(frame)
        frame = frame.f_back
    stack.reverse()
    traceback.print_exc()
734 735 736
    for frame in stack:
        print "\nFrame %s in %s at line %s" % (frame.f_code.co_name,
                                             frame.f_code.co_filename,
737
                                             frame.f_lineno)
738
        for key, value in frame.f_locals.items():
739
            print "\t%20s = " % key,
740
            try:
741
                print value
742
            except:
743
                print "<unable to print>"
744 745 746

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

J
James Troup 已提交
747 748
def try_with_debug(function):
    try:
749
        function()
J
James Troup 已提交
750
    except SystemExit:
751
        raise
J
James Troup 已提交
752
    except:
753
        print_exc()
J
James Troup 已提交
754 755 756

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

J
James Troup 已提交
757 758 759 760 761
# 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":
762
        return 0
J
James Troup 已提交
763
    elif a == "source":
764
        return -1
J
James Troup 已提交
765
    elif b == "source":
766
        return 1
J
James Troup 已提交
767

768
    return cmp (a, b)
J
James Troup 已提交
769 770 771

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

772 773
# Split command line arguments which can be separated by either commas
# or whitespace.  If dwim is set, it will complain about string ending
774
# in comma since this usually means someone did 'dak ls -a i386, m68k
775 776 777 778 779
# 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:
780
        return s.split()
781 782
    else:
        if s[-1:] == "," and dwim:
783 784
            fubar("split_args: found trailing comma, spurious space maybe?")
        return s.split(",")
785 786 787

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

788 789 790 791 792 793 794
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):
795 796 797 798 799
    cmd = ['/bin/sh', '-c', cmd]
    p2cread, p2cwrite = os.pipe()
    c2pread, c2pwrite = os.pipe()
    errout, errin = os.pipe()
    pid = os.fork()
800 801
    if pid == 0:
        # Child
802 803 804 805 806 807
        os.close(0)
        os.close(1)
        os.dup(p2cread)
        os.dup(c2pwrite)
        os.close(2)
        os.dup(errin)
808 809 810
        for i in range(3, 256):
            if i != status_write:
                try:
811
                    os.close(i)
812
                except:
813
                    pass
814
        try:
815
            os.execvp(cmd[0], cmd)
816
        finally:
817
            os._exit(1)
818

819
    # Parent
820
    os.close(p2cread)
821 822
    os.dup2(c2pread, c2pwrite)
    os.dup2(errout, errin)
823

824
    output = status = ""
825
    while 1:
826 827
        i, o, e = select.select([c2pwrite, errin, status_read], [], [])
        more_data = []
828
        for fd in i:
829
            r = os.read(fd, 8196)
830
            if len(r) > 0:
831
                more_data.append(fd)
832
                if fd == c2pwrite or fd == errin:
833
                    output += r
834
                elif fd == status_read:
835
                    status += r
836
                else:
837
                    fubar("Unexpected file descriptor [%s] returned from select\n" % (fd))
838 839 840
        if not more_data:
            pid, exit_status = os.waitpid(pid, 0)
            try:
841 842 843 844 845 846 847
                os.close(status_write)
                os.close(status_read)
                os.close(c2pread)
                os.close(c2pwrite)
                os.close(p2cwrite)
                os.close(errin)
                os.close(errout)
848
            except:
849 850
                pass
            break
851

852
    return output, status, exit_status
853

854
################################################################################
855

856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872
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 已提交
873
        if keywords.has_key(keyword) and keyword not in [ "NODATA", "SIGEXPIRED", "KEYEXPIRED" ]:
874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891
            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:
892
        keyring = Cnf.ValueList("Dinstall::GPGKeyring")[0]
893 894 895 896 897 898 899 900 901

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

903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926
    # 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 ""

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

927
def gpg_keyring_args(keyrings=None):
928 929 930 931 932 933 934
    if not keyrings:
        keyrings = Cnf.ValueList("Dinstall::GPGKeyring")

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

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

935
def check_signature (sig_filename, reject, data_filename="", keyrings=None, autofetch=None):
936 937 938 939 940
    """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,
941
the second is an optional prefix string.  It's possible for reject()
942 943 944
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
945 946 947
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."""
948 949

    # Ensure the filename contains no shell meta-characters or other badness
950
    if not re_taint_free.match(sig_filename):
951 952
        reject("!!WARNING!! tainted signature filename: '%s'." % (sig_filename))
        return None
953 954

    if data_filename and not re_taint_free.match(data_filename):
955 956
        reject("!!WARNING!! tainted data filename: '%s'." % (data_filename))
        return None
957 958

    if not keyrings:
959
        keyrings = Cnf.ValueList("Dinstall::GPGKeyring")
960

961 962 963 964 965 966 967 968 969
    # 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

970 971
    # Build the command line
    status_read, status_write = os.pipe(); 
972 973 974
    cmd = "gpgv --status-fd %s %s %s %s" % (
        status_write, gpg_keyring_args(keyrings), sig_filename, data_filename)

975
    # Invoke gpgv on the file
976
    (output, status, exit_status) = gpgv_get_status_output(cmd, status_read, status_write)
977 978

    # Process the status-fd output
979
    (keywords, internal_error) = process_gpgv_output(status)
980 981 982

    # If we failed to parse the status-fd output, let's just whine and bail now
    if internal_error:
983 984 985 986
        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
987

988
    bad = ""
989 990
    # Now check for obviously bad things in the processed output
    if keywords.has_key("KEYREVOKED"):
991 992
        reject("The key used to sign %s has been revoked." % (sig_filename))
        bad = 1
993
    if keywords.has_key("BADSIG"):
994 995
        reject("bad signature on %s." % (sig_filename))
        bad = 1
996
    if keywords.has_key("ERRSIG") and not keywords.has_key("NO_PUBKEY"):
997 998
        reject("failed to check signature on %s." % (sig_filename))
        bad = 1
999
    if keywords.has_key("NO_PUBKEY"):
1000
        args = keywords["NO_PUBKEY"]
1001
        if len(args) >= 1:
1002 1003 1004
            key = args[0]
        reject("The key (0x%s) used to sign %s wasn't found in the keyring(s)." % (key, sig_filename))
        bad = 1
1005
    if keywords.has_key("BADARMOR"):
1006 1007
        reject("ASCII armour of signature was corrupt in %s." % (sig_filename))
        bad = 1
1008
    if keywords.has_key("NODATA"):
1009 1010
        reject("no signature found in %s." % (sig_filename))
        bad = 1
A
Anthony Towns 已提交
1011
    if keywords.has_key("KEYEXPIRED") and not keywords.has_key("GOODSIG"):
1012 1013 1014
        args = keywords["KEYEXPIRED"]
        if len(args) >= 1:
            key = args[0]
A
Anthony Towns 已提交
1015 1016
        reject("The key (0x%s) used to sign %s has expired." % (key, sig_filename))
        bad = 1
1017 1018

    if bad:
1019
        return None
1020 1021 1022

    # Next check gpgv exited with a zero return code
    if exit_status:
1023
        reject("gpgv failed while checking %s." % (sig_filename))
1024
        if status.strip():
1025
            reject(prefix_multi_line_string(status, " [GPG status-fd output:] "), "")
1026
        else:
1027 1028
            reject(prefix_multi_line_string(output, " [GPG output:] "), "")
        return None
1029 1030 1031

    # Sanity check the good stuff we expect
    if not keywords.has_key("VALIDSIG"):
1032 1033
        reject("signature on %s does not appear to be valid [No VALIDSIG]." % (sig_filename))
        bad = 1
1034
    else:
1035
        args = keywords["VALIDSIG"]
1036
        if len(args) < 1:
1037 1038
            reject("internal error while checking signature on %s." % (sig_filename))
            bad = 1
1039
        else:
1040
            fingerprint = args[0]
1041
    if not keywords.has_key("GOODSIG"):
1042 1043
        reject("signature on %s does not appear to be valid [No GOODSIG]." % (sig_filename))
        bad = 1
1044
    if not keywords.has_key("SIG_ID"):
1045 1046
        reject("signature on %s does not appear to be valid [No SIG_ID]." % (sig_filename))
        bad = 1
1047 1048 1049 1050

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

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

    if bad:
1059
        return None
1060
    else:
1061
        return fingerprint
1062 1063 1064

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

1065 1066 1067
# Inspired(tm) by http://www.zopelabs.com/cookbook/1022242603

def wrap(paragraph, max_length, prefix=""):
1068 1069 1070 1071
    line = ""
    s = ""
    have_started = 0
    words = paragraph.split()
1072 1073

    for word in words:
1074
        word_size = len(word)
1075 1076
        if word_size > max_length:
            if have_started:
1077 1078
                s += line + '\n' + prefix
            s += word + '\n' + prefix
1079 1080
        else:
            if have_started:
1081
                new_length = len(line) + word_size + 1
1082
                if new_length > max_length:
1083 1084
                    s += line + '\n' + prefix
                    line = word
1085
                else:
1086
                    line += ' ' + word
1087
            else:
1088 1089
                line = word
        have_started = 1
1090 1091

    if have_started:
1092
        s += line
1093

1094
    return s
1095 1096 1097 1098 1099 1100

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

# Relativize an absolute symlink from 'src' -> 'dest' relative to 'root'.
# Returns fixed 'src'
def clean_symlink (src, dest, root):
1101 1102 1103 1104 1105
    src = src.replace(root, '', 1)
    dest = dest.replace(root, '', 1)
    dest = os.path.dirname(dest)
    new_src = '../' * len(dest.split('/'))
    return new_src + src
1106 1107 1108

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

1109 1110 1111 1112 1113 1114
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:
1115 1116
        old_tempdir = tempfile.tempdir
        tempfile.tempdir = directory
1117

1118
    filename = tempfile.mktemp()
1119 1120

    if dotprefix:
1121 1122 1123
        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)
1124 1125

    if directory:
1126
        tempfile.tempdir = old_tempdir
1127

1128
    return filename
1129 1130 1131

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

1132
apt_pkg.init()
1133

1134 1135
Cnf = apt_pkg.newConfiguration()
apt_pkg.ReadConfigFileISC(Cnf,default_config)
1136 1137

if which_conf_file() != default_config:
1138
	apt_pkg.ReadConfigFileISC(Cnf,which_conf_file())
1139 1140

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