utils.py 38.5 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
J
Joerg Jaspert 已提交
28
from dak_exceptions import *
29 30

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

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

40 41 42
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]+$")
43

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

47
re_srchasver = re.compile(r"^(\S+)\s+\((\S+)\)$")
48
re_verwithext = re.compile(r"^(\d+)(?:\.(\d+))(?:\s+\((\S+)\))?$")
J
James Troup 已提交
49

50 51
re_srchasver = re.compile(r"^(\S+)\s+\((\S+)\)$")

52 53
default_config = "/etc/dak/dak.conf"
default_apt_config = "/etc/dak/apt.conf"
J
James Troup 已提交
54

T
Thomas Viehmann 已提交
55
alias_cache = None
56
key_uid_email_cache = {}
T
Thomas Viehmann 已提交
57

58
################################################################################
J
James Troup 已提交
59

60
def open_file(filename, mode='r'):
J
James Troup 已提交
61
    try:
62
        f = open(filename, mode)
J
James Troup 已提交
63
    except IOError:
J
Joerg Jaspert 已提交
64
        raise CantOpenError, filename
J
James Troup 已提交
65 66
    return f

67
################################################################################
J
James Troup 已提交
68

69 70
def our_raw_input(prompt=""):
    if prompt:
71 72
        sys.stdout.write(prompt)
    sys.stdout.flush()
J
James Troup 已提交
73
    try:
74 75
        ret = raw_input()
        return ret
J
James Troup 已提交
76
    except EOFError:
77 78
        sys.stderr.write("\nUser interrupt (^D).\n")
        raise SystemExit
J
James Troup 已提交
79

80
################################################################################
J
James Troup 已提交
81

J
James Troup 已提交
82
def extract_component_from_section(section):
83
    component = ""
J
James Troup 已提交
84

85
    if section.find('/') != -1:
86
        component = section.split('/')[0]
87 88

    # Expand default component
J
James Troup 已提交
89
    if component == "":
90
        if Cnf.has_key("Component::%s" % section):
91
            component = section
92
        else:
93
            component = "main"
J
James Troup 已提交
94

95
    return (section, component)
J
James Troup 已提交
96

97 98
################################################################################

99 100 101 102
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 已提交
103

104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
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 已提交
119

120 121
    error = ""
    changes = {}
122

123 124
    changes_in = open_file(filename)
    lines = changes_in.readlines()
J
James Troup 已提交
125

126
    if not lines:
J
Joerg Jaspert 已提交
127
        raise ParseChangesError, "[Empty changes file]"
128

J
James Troup 已提交
129 130
    # Reindex by line number so we can easily verify the format of
    # .dsc files...
131 132
    index = 0
    indexed_lines = {}
J
James Troup 已提交
133
    for line in lines:
134 135
        index += 1
        indexed_lines[index] = line[:-1]
J
James Troup 已提交
136

137
    inside_signature = 0
J
James Troup 已提交
138

139 140 141
    num_of_lines = len(indexed_lines.keys())
    index = 0
    first = -1
142
    while index < num_of_lines:
143 144
        index += 1
        line = indexed_lines[index]
J
James Troup 已提交
145
        if line == "":
146
            if signing_rules == 1:
147
                index += 1
148
                if index > num_of_lines:
J
Joerg Jaspert 已提交
149
                    raise InvalidDscError, index
150
                line = indexed_lines[index]
151
                if not line.startswith("-----BEGIN PGP SIGNATURE"):
J
Joerg Jaspert 已提交
152
                    raise InvalidDscError, index
153 154
                inside_signature = 0
                break
155
            else:
156
                continue
157
        if line.startswith("-----BEGIN PGP SIGNATURE"):
158
            break
159
        if line.startswith("-----BEGIN PGP SIGNED MESSAGE"):
160
            inside_signature = 1
161
            if signing_rules == 1:
162
                while index < num_of_lines and line != "":
163 164 165
                    index += 1
                    line = indexed_lines[index]
            continue
166
        # If we're not inside the signed data, don't process anything
167
        if signing_rules >= 0 and not inside_signature:
168 169
            continue
        slf = re_single_line_field.match(line)
J
James Troup 已提交
170
        if slf:
171 172
            field = slf.groups()[0].lower()
            changes[field] = slf.groups()[1]
173
            first = 1
174
            continue
J
James Troup 已提交
175
        if line == " .":
176 177 178
            changes[field] += '\n'
            continue
        mlf = re_multi_line_field.match(line)
J
James Troup 已提交
179
        if mlf:
180
            if first == -1:
J
Joerg Jaspert 已提交
181
                raise ParseChangesError, "'%s'\n [Multi-line field continuing on from nothing?]" % (line)
182
            if first == 1 and changes[field] != "":
183 184
                changes[field] += '\n'
            first = 0
185
            changes[field] += mlf.groups()[0] + '\n'
186
            continue
187
        error += line
J
James Troup 已提交
188

189
    if signing_rules == 1 and inside_signature:
J
Joerg Jaspert 已提交
190
        raise InvalidDscError, index
J
James Troup 已提交
191

192 193
    changes_in.close()
    changes["filecontents"] = "".join(lines)
J
James Troup 已提交
194

195 196
    if changes.has_key("source"):
        # Strip the source version in brackets from the source field,
197
        # put it in the "source-version" field instead.
198
        srcver = re_srchasver.search(changes["source"])
199
        if srcver:
200
            changes["source"] = srcver.group(1)
201
            changes["source-version"] = srcver.group(2)
202

203
    if error:
J
Joerg Jaspert 已提交
204
        raise ParseChangesError, error
J
James Troup 已提交
205

206
    return changes
J
James Troup 已提交
207

208
################################################################################
J
James Troup 已提交
209 210 211

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

212
def build_file_list(changes, is_a_dsc=0, field="files", hashname="md5sum"):
213
    files = {}
214 215

    # Make sure we have a Files: field to parse...
216
    if not changes.has_key(field):
J
Joerg Jaspert 已提交
217
        raise NoFilesFieldError
218 219

    # Make sure we recognise the format of the Files: field
220 221
    format = re_verwithext.search(changes.get("format", "0.0"))
    if not format:
J
Joerg Jaspert 已提交
222
        raise UnknownFormatError, "%s" % (changes.get("format","0.0"))
223 224 225 226

    format = format.groups()
    if format[1] == None:
        format = int(float(format[0])), 0, format[2]
227
    else:
228 229 230
        format = int(format[0]), int(format[1]), format[2]
    if format[2] == None:
        format = format[:2]
231 232 233

    if is_a_dsc:
        if format != (1,0):
J
Joerg Jaspert 已提交
234
            raise UnknownFormatError, "%s" % (changes.get("format","0.0"))
235 236
    else:
        if (format < (1,5) or format > (1,8)):
J
Joerg Jaspert 已提交
237
            raise UnknownFormatError, "%s" % (changes.get("format","0.0"))
238
        if field != "files" and format < (1,8):
J
Joerg Jaspert 已提交
239
            raise UnknownFormatError, "%s" % (changes.get("format","0.0"))
240 241

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

243
    # Parse each entry/line:
244
    for i in changes[field].split('\n'):
245
        if not i:
246 247 248
            break
        s = i.split()
        section = priority = ""
J
sync  
James Troup 已提交
249
        try:
250
            if includes_section:
251
                (md5, size, section, priority, name) = s
252 253
            else:
                (md5, size, name) = s
J
sync  
James Troup 已提交
254
        except ValueError:
J
Joerg Jaspert 已提交
255
            raise ParseChangesError, i
J
James Troup 已提交
256

257
        if section == "":
258
            section = "-"
259
        if priority == "":
260
            priority = "-"
J
James Troup 已提交
261

262
        (section, component) = extract_component_from_section(section)
J
James Troup 已提交
263

264
        files[name] = Dict(size=size, section=section,
265
                           priority=priority, component=component)
266
        files[name][hashname] = md5
J
James Troup 已提交
267 268 269

    return files

270
################################################################################
J
James Troup 已提交
271

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

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

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

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

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

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

    # Get an RFC2047 compliant version of the name
332
    rfc2047_name = rfc2047_encode(name)
333 334

    # Force the name to be UTF-8
335
    name = force_to_utf8(name)
336 337

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

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

347
    return (rfc822_maint, rfc2047_maint, name, email)
J
James Troup 已提交
348

349
################################################################################
J
James Troup 已提交
350 351

# sendmail wrapper, takes _either_ a message string or a file as arguments
352
def send_mail (message, filename=""):
353 354 355 356 357 358 359 360 361 362
        # If we've been passed a string dump it into a temporary file
    if message:
        filename = tempfile.mktemp()
        fd = os.open(filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0700)
        os.write (fd, message)
        os.close (fd)

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

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

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

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

379
################################################################################
J
James Troup 已提交
380

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

404
def copy (src, dest, overwrite = 0, perms = 0664):
J
James Troup 已提交
405
    if os.path.exists(dest) and os.path.isdir(dest):
406
        dest_dir = dest
J
James Troup 已提交
407
    else:
408
        dest_dir = os.path.dirname(dest)
J
James Troup 已提交
409
    if not os.path.exists(dest_dir):
410 411 412
        umask = os.umask(00000)
        os.makedirs(dest_dir, 02775)
        os.umask(umask)
413
    #print "Copying %s to %s..." % (src, dest)
J
James Troup 已提交
414
    if os.path.exists(dest) and os.path.isdir(dest):
415
        dest += '/' + os.path.basename(src)
416 417 418
    # Don't overwrite unless forced to
    if os.path.exists(dest):
        if not overwrite:
J
Joerg Jaspert 已提交
419
            raise FileExistsError
420 421
        else:
            if not os.access(dest, os.W_OK):
J
Joerg Jaspert 已提交
422
                raise CantOverwriteError
423 424
    shutil.copy2(src, dest)
    os.chmod(dest, perms)
J
James Troup 已提交
425

426
################################################################################
J
James Troup 已提交
427 428

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

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

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

T
Thomas Viehmann 已提交
450 451 452 453 454 455 456 457
def which_alias_file():
    hostname = socket.gethostbyaddr(socket.gethostname())[0]
    aliasfn = '/var/lib/misc/'+hostname+'/forward-alias'
    if os.path.exists(aliasfn):
        return aliasfn
    else:
        return None

458
################################################################################
J
James Troup 已提交
459

460 461 462 463
# Escape characters which have meaning to SQL's regex comparison operator ('~')
# (woefully incomplete)

def regex_safe (s):
464 465
    s = s.replace('+', '\\\\+')
    s = s.replace('.', '\\\\.')
466 467
    return s

468
################################################################################
469

J
James Troup 已提交
470
# Perform a substition of template
471
def TemplateSubst(map, filename):
472 473
    file = open_file(filename)
    template = file.read()
474
    for x in map.keys():
475 476 477
        template = template.replace(x,map[x])
    file.close()
    return template
J
James Troup 已提交
478

479
################################################################################
J
James Troup 已提交
480 481

def fubar(msg, exit_code=1):
482 483
    sys.stderr.write("E: %s\n" % (msg))
    sys.exit(exit_code)
J
James Troup 已提交
484 485

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

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

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

495
################################################################################
J
James Troup 已提交
496

J
James Troup 已提交
497
def size_type (c):
498
    t  = " B"
499
    if c > 10240:
500 501
        c = c / 1024
        t = " KB"
502
    if c > 10240:
503 504
        c = c / 1024
        t = " MB"
J
James Troup 已提交
505
    return ("%d%s" % (c, t))
506 507 508 509

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

def cc_fix_changes (changes):
510
    o = changes.get("architecture", "")
511
    if o:
512 513
        del changes["architecture"]
    changes["architecture"] = {}
514
    for j in o.split():
515
        changes["architecture"][j] = 1
516

517
# Sort by source name, source version, 'have source', and then by filename
518 519
def changes_compare (a, b):
    try:
520
        a_changes = parse_changes(a)
521
    except:
522
        return -1
523 524

    try:
525
        b_changes = parse_changes(b)
526
    except:
527
        return 1
J
James Troup 已提交
528

529 530
    cc_fix_changes (a_changes)
    cc_fix_changes (b_changes)
531 532

    # Sort by source name
533 534 535
    a_source = a_changes.get("source")
    b_source = b_changes.get("source")
    q = cmp (a_source, b_source)
536
    if q:
537
        return q
538 539

    # Sort by source version
540 541 542
    a_version = a_changes.get("version", "0")
    b_version = b_changes.get("version", "0")
    q = apt_pkg.VersionCompare(a_version, b_version)
543
    if q:
544
        return q
545

546
    # Sort by 'have source'
547 548
    a_has_source = a_changes["architecture"].get("source")
    b_has_source = b_changes["architecture"].get("source")
549
    if a_has_source and not b_has_source:
550
        return -1
551
    elif b_has_source and not a_has_source:
552
        return 1
553

554
    # Fall back to sort by filename
555
    return cmp(a, b)
556 557

################################################################################
558 559

def find_next_free (dest, too_many=100):
560 561
    extra = 0
    orig_dest = dest
562
    while os.path.exists(dest) and extra < too_many:
563 564
        dest = orig_dest + '.' + repr(extra)
        extra += 1
565
    if extra >= too_many:
J
Joerg Jaspert 已提交
566
        raise NoFreeFilenameError
567
    return dest
J
James Troup 已提交
568

569
################################################################################
570 571

def result_join (original, sep = '\t'):
572
    list = []
573 574
    for i in xrange(len(original)):
        if original[i] == None:
575
            list.append("")
576
        else:
577 578
            list.append(original[i])
    return sep.join(list)
579

580 581
################################################################################

582
def prefix_multi_line_string(str, prefix, include_blank_lines=0):
583
    out = ""
584
    for line in str.split('\n'):
585
        line = line.strip()
586
        if line or include_blank_lines:
587
            out += "%s%s\n" % (prefix, line)
588 589
    # Strip trailing new line
    if out:
590 591
        out = out[:-1]
    return out
592

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

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

610
    orig_filename = filename
611
    if filename.endswith(".dak"):
612
        filename = filename[:-4]+".changes"
613

614
    if not filename.endswith(".changes"):
615
        error = "invalid file type; not a changes file"
616
    else:
617 618
        if not os.access(filename,os.R_OK):
            if os.path.exists(filename):
619
                error = "permission denied"
620
            else:
621
                error = "file not found"
622 623

    if error:
624
        if require_changes == 1:
625
            fubar("%s: %s." % (orig_filename, error))
626
        elif require_changes == 0:
627 628
            warn("Skipping %s - %s" % (orig_filename, error))
            return None
629
        else: # We only care about the .dak file
630
            return filename
631
    else:
632
        return filename
633 634 635

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

636
def real_arch(arch):
637
    return (arch != "source" and arch != "all")
638 639 640

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

641
def join_with_commas_and(list):
642 643 644
    if len(list) == 0: return "nothing"
    if len(list) == 1: return list[0]
    return ", ".join(list[:-1]) + " and " + list[-1]
645 646 647

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

648
def pp_deps (deps):
649
    pp_deps = []
650
    for atom in deps:
651
        (pkg, version, constraint) = atom
652
        if constraint:
653
            pp_dep = "%s (%s %s)" % (pkg, constraint, version)
654
        else:
655 656 657
            pp_dep = pkg
        pp_deps.append(pp_dep)
    return " |".join(pp_deps)
658 659 660

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

661
def get_conf():
662
    return Cnf
663 664 665

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

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

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

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

722
    return (con_suites, con_architectures, con_components, check_source)
723 724 725

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

726 727 728 729
# Inspired(tm) by Bryn Keller's print_exc_plus (See
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52215)

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

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

J
James Troup 已提交
753 754
def try_with_debug(function):
    try:
755
        function()
J
James Troup 已提交
756
    except SystemExit:
757
        raise
J
James Troup 已提交
758
    except:
759
        print_exc()
J
James Troup 已提交
760 761 762

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

J
James Troup 已提交
763 764 765 766 767
# 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":
768
        return 0
J
James Troup 已提交
769
    elif a == "source":
770
        return -1
J
James Troup 已提交
771
    elif b == "source":
772
        return 1
J
James Troup 已提交
773

774
    return cmp (a, b)
J
James Troup 已提交
775 776 777

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

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

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

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

825
    # Parent
826
    os.close(p2cread)
827 828
    os.dup2(c2pread, c2pwrite)
    os.dup2(errout, errin)
829

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

858
    return output, status, exit_status
859

860
################################################################################
861

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

    # 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
905
    status_read, status_write = os.pipe();
906 907
    cmd = "gpgv --status-fd %s --keyring /dev/null %s" % (status_write, filename)
    (_, status, _) = gpgv_get_status_output(cmd, status_read, status_write)
908

909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932
    # 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 ""

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

933 934 935 936 937 938 939 940
def gpg_keyring_args(keyrings=None):
    if not keyrings:
        keyrings = Cnf.ValueList("Dinstall::GPGKeyring")

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

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

941
def check_signature (sig_filename, reject, data_filename="", keyrings=None, autofetch=None):
942 943 944 945 946
    """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,
947
the second is an optional prefix string.  It's possible for reject()
948 949 950
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
951 952 953
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."""
954 955

    # Ensure the filename contains no shell meta-characters or other badness
956
    if not re_taint_free.match(sig_filename):
957 958
        reject("!!WARNING!! tainted signature filename: '%s'." % (sig_filename))
        return None
959 960

    if data_filename and not re_taint_free.match(data_filename):
961 962
        reject("!!WARNING!! tainted data filename: '%s'." % (data_filename))
        return None
963 964

    if not keyrings:
965
        keyrings = Cnf.ValueList("Dinstall::GPGKeyring")
966

967 968 969 970 971 972 973 974 975
    # 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

976
    # Build the command line
977
    status_read, status_write = os.pipe();
978 979 980
    cmd = "gpgv --status-fd %s %s %s %s" % (
        status_write, gpg_keyring_args(keyrings), sig_filename, data_filename)

981
    # Invoke gpgv on the file
982
    (output, status, exit_status) = gpgv_get_status_output(cmd, status_read, status_write)
983 984

    # Process the status-fd output
985
    (keywords, internal_error) = process_gpgv_output(status)
986 987 988

    # If we failed to parse the status-fd output, let's just whine and bail now
    if internal_error:
989 990 991 992
        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
993

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

    if bad:
1025
        return None
1026 1027 1028

    # Next check gpgv exited with a zero return code
    if exit_status:
1029
        reject("gpgv failed while checking %s." % (sig_filename))
1030
        if status.strip():
1031
            reject(prefix_multi_line_string(status, " [GPG status-fd output:] "), "")
1032
        else:
1033 1034
            reject(prefix_multi_line_string(output, " [GPG output:] "), "")
        return None
1035 1036 1037

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

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

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

    if bad:
1065
        return None
1066
    else:
1067
        return fingerprint
1068 1069 1070

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

1071
def gpg_get_key_addresses(fingerprint):
1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086
    """retreive email addresses from gpg key uids for a given fingerprint"""
    addresses = key_uid_email_cache.get(fingerprint)
    if addresses != None:
        return addresses
    addresses = set()
    cmd = "gpg --no-default-keyring %s --fingerprint %s" \
                % (gpg_keyring_args(), fingerprint)
    (result, output) = commands.getstatusoutput(cmd)
    if result == 0:
        for l in output.split('\n'):
            m = re_gpg_uid.match(l)
            if m:
                addresses.add(m.group(1))
    key_uid_email_cache[fingerprint] = addresses
    return addresses
1087 1088 1089

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

1090 1091 1092
# Inspired(tm) by http://www.zopelabs.com/cookbook/1022242603

def wrap(paragraph, max_length, prefix=""):
1093 1094 1095 1096
    line = ""
    s = ""
    have_started = 0
    words = paragraph.split()
1097 1098

    for word in words:
1099
        word_size = len(word)
1100 1101
        if word_size > max_length:
            if have_started:
1102 1103
                s += line + '\n' + prefix
            s += word + '\n' + prefix
1104 1105
        else:
            if have_started:
1106
                new_length = len(line) + word_size + 1
1107
                if new_length > max_length:
1108 1109
                    s += line + '\n' + prefix
                    line = word
1110
                else:
1111
                    line += ' ' + word
1112
            else:
1113 1114
                line = word
        have_started = 1
1115 1116

    if have_started:
1117
        s += line
1118

1119
    return s
1120 1121 1122 1123 1124 1125

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

# Relativize an absolute symlink from 'src' -> 'dest' relative to 'root'.
# Returns fixed 'src'
def clean_symlink (src, dest, root):
1126 1127 1128 1129 1130
    src = src.replace(root, '', 1)
    dest = dest.replace(root, '', 1)
    dest = os.path.dirname(dest)
    new_src = '../' * len(dest.split('/'))
    return new_src + src
1131 1132 1133

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

1134 1135 1136 1137 1138 1139
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:
1140 1141
        old_tempdir = tempfile.tempdir
        tempfile.tempdir = directory
1142

1143
    filename = tempfile.mktemp()
1144 1145

    if dotprefix:
1146 1147 1148
        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)
1149 1150

    if directory:
1151
        tempfile.tempdir = old_tempdir
1152

1153
    return filename
1154 1155 1156

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

T
Thomas Viehmann 已提交
1157 1158 1159
# checks if the user part of the email is listed in the alias file

def is_email_alias(email):
T
Thomas Viehmann 已提交
1160 1161 1162 1163 1164 1165 1166 1167 1168
    global alias_cache
    if alias_cache == None:
        aliasfn = which_alias_file()
        alias_cache = set()
        if aliasfn:
            for l in open(aliasfn):
                alias_cache.add(l.split(':')[0])
    uid = email.split('@')[0]
    return uid in alias_cache
T
Thomas Viehmann 已提交
1169 1170 1171

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

1172
apt_pkg.init()
1173

1174 1175
Cnf = apt_pkg.newConfiguration()
apt_pkg.ReadConfigFileISC(Cnf,default_config)
1176 1177

if which_conf_file() != default_config:
1178
    apt_pkg.ReadConfigFileISC(Cnf,which_conf_file())
1179 1180

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