utils.py 42.7 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, stat
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 59 60 61
# (hashname, function, earliest_changes_version)
known_hashes = [("sha1", apt_pkg.sha1sum, (1, 8)),
                ("sha256", apt_pkg.sha256sum, (1, 8))]

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

64
def open_file(filename, mode='r'):
J
James Troup 已提交
65
    try:
66
        f = open(filename, mode)
J
James Troup 已提交
67
    except IOError:
J
Joerg Jaspert 已提交
68
        raise CantOpenError, filename
J
James Troup 已提交
69 70
    return f

71
################################################################################
J
James Troup 已提交
72

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

84
################################################################################
J
James Troup 已提交
85

J
James Troup 已提交
86
def extract_component_from_section(section):
87
    component = ""
J
James Troup 已提交
88

89
    if section.find('/') != -1:
90
        component = section.split('/')[0]
91 92

    # Expand default component
J
James Troup 已提交
93
    if component == "":
94
        if Cnf.has_key("Component::%s" % section):
95
            component = section
96
        else:
97
            component = "main"
J
James Troup 已提交
98

99
    return (section, component)
J
James Troup 已提交
100

101 102
################################################################################

103 104 105 106
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 已提交
107

108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
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 已提交
123

124 125
    error = ""
    changes = {}
126

127 128
    changes_in = open_file(filename)
    lines = changes_in.readlines()
J
James Troup 已提交
129

130
    if not lines:
J
Joerg Jaspert 已提交
131
        raise ParseChangesError, "[Empty changes file]"
132

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

141
    inside_signature = 0
J
James Troup 已提交
142

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

193
    if signing_rules == 1 and inside_signature:
J
Joerg Jaspert 已提交
194
        raise InvalidDscError, index
J
James Troup 已提交
195

196 197
    changes_in.close()
    changes["filecontents"] = "".join(lines)
J
James Troup 已提交
198

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

207
    if error:
J
Joerg Jaspert 已提交
208
        raise ParseChangesError, error
J
James Troup 已提交
209

210
    return changes
J
James Troup 已提交
211

212
################################################################################
J
James Troup 已提交
213

214 215 216 217 218 219 220 221 222
def create_hash (lfiles, key, testfn, basedict = None):
    rejmsg = []
    for f in lfiles.keys():
        try:
            file_handle = open_file(f)
        except CantOpenError:
            rejmsg.append("Could not open file %s for checksumming" % (f))

        # Check hash
223
        if basedict and basedict.has_key(f):
224
            basedict[f]['%ssum' % key] = testfn(file_handle)
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
        file_handle.close()

    return rejmsg

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

def check_hash (where, lfiles, key, testfn, basedict = None):
    rejmsg = []
    if basedict:
        for f in basedict.keys():
            if f not in lfiles:
                rejmsg.append("%s: no %s checksum" % (f, key))

    for f in lfiles.keys():
        if basedict and f not in basedict:
            rejmsg.append("%s: extraneous entry in %s checksums" % (f, key))

        try:
            file_handle = open_file(f)
        except CantOpenError:
            continue

        # Check hash
        if testfn(file_handle) != lfiles[f][key]:
            rejmsg.append("%s: %s check failed." % (f, key))
        file_handle.close()
        # Store the hashes for later use
252 253
        if basedict:
            basedict[f]['%ssum' % key] = lfiles[f][key]
254 255 256 257 258 259 260 261 262 263 264
        # Check size
        actual_size = os.stat(f)[stat.ST_SIZE]
        size = int(lfiles[f]["size"])
        if size != actual_size:
            rejmsg.append("%s: actual file size (%s) does not match size (%s) in %s"
                   % (f, actual_size, size, where))

    return rejmsg

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

M
Mark Hymers 已提交
265 266 267 268 269 270 271 272
def ensure_hashes(changes, dsc, files, dsc_files):
    # Make sure we recognise the format of the Files: field
    format = changes.get("format", "0.0").split(".",1)
    if len(format) == 2:
        format = int(format[0]), int(format[1])
    else:
        format = int(float(format[0])), 0

273
    rejmsg = []
M
Mark Hymers 已提交
274
    for x in changes:
275 276 277 278 279
        if x.startswith("checksum-"):
            h = x.split("-",1)[1]
            if h not in dict(known_hashes):
                rejmsg.append("Unsupported checksum field in .changes" % (h))

M
Mark Hymers 已提交
280
    for x in dsc:
281 282 283 284 285
        if x.startswith("checksum-"):
            h = x.split("-",1)[1]
            if h not in dict(known_hashes):
                rejmsg.append("Unsupported checksum field in .dsc" % (h))

286 287 288 289

    fs_m = build_file_list(changes, 0)
    fs_md = build_file_list(dsc, 1)

290 291 292 293 294
    # We have to calculate the hash if we have an earlier changes version than
    # the hash appears in rather than require it exist in the changes file
    # I hate backwards compatibility
    for h,f,v in known_hashes:
        try:
295

296
            if format < v:
297
                for m in create_hash(fs_m, h, f, files):
298 299
                    rejmsg.append(m)
            else:
300
                fs = build_file_list(changes, 0, "checksums-%s" % h, h)
M
Mark Hymers 已提交
301
                for m in check_hash(".changes %s" % (h), fs, h, f, files):
302 303 304 305 306 307 308 309
                    rejmsg.append(m)
        except NoFilesFieldError:
            rejmsg.append("No Checksums-%s: field in .changes" % (h))
        except UnknownFormatError, format:
            rejmsg.append("%s: unknown format of .changes" % (format))
        except ParseChangesError, line:
            rejmsg.append("parse error for Checksums-%s in .changes, can't grok: %s." % (h, line))

M
Mark Hymers 已提交
310
        if "source" not in changes["architecture"]: continue
311 312 313

        try:
            if format < v:
314
                for m in create_hash(fs_md, h, f, dsc_files):
315 316
                    rejmsg.append(m)
            else:
317
                fs = build_file_list(dsc, 1, "checksums-%s" % h, h)
M
Mark Hymers 已提交
318
                for m in check_hash(".dsc %s" % (h), fs, h, f, dsc_files):
319 320 321 322 323 324 325 326 327 328 329 330
                    rejmsg.append(m)
        except UnknownFormatError, format:
            rejmsg.append("%s: unknown format of .dsc" % (format))
        except NoFilesFieldError:
            rejmsg.append("No Checksums-%s: field in .dsc" % (h))
        except ParseChangesError, line:
            rejmsg.append("parse error for Checksums-%s in .dsc, can't grok: %s." % (h, line))

    return rejmsg

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

J
James Troup 已提交
331 332
# Dropped support for 1.4 and ``buggy dchanges 3.4'' (?!) compared to di.pl

333
def build_file_list(changes, is_a_dsc=0, field="files", hashname="md5sum"):
334
    files = {}
335 336

    # Make sure we have a Files: field to parse...
337
    if not changes.has_key(field):
J
Joerg Jaspert 已提交
338
        raise NoFilesFieldError
339 340

    # Make sure we recognise the format of the Files: field
341 342
    format = re_verwithext.search(changes.get("format", "0.0"))
    if not format:
J
Joerg Jaspert 已提交
343
        raise UnknownFormatError, "%s" % (changes.get("format","0.0"))
344 345 346 347

    format = format.groups()
    if format[1] == None:
        format = int(float(format[0])), 0, format[2]
348
    else:
349 350 351
        format = int(format[0]), int(format[1]), format[2]
    if format[2] == None:
        format = format[:2]
352 353 354

    if is_a_dsc:
        if format != (1,0):
J
Joerg Jaspert 已提交
355
            raise UnknownFormatError, "%s" % (changes.get("format","0.0"))
356 357
    else:
        if (format < (1,5) or format > (1,8)):
J
Joerg Jaspert 已提交
358
            raise UnknownFormatError, "%s" % (changes.get("format","0.0"))
359
        if field != "files" and format < (1,8):
J
Joerg Jaspert 已提交
360
            raise UnknownFormatError, "%s" % (changes.get("format","0.0"))
361 362

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

364
    # Parse each entry/line:
365
    for i in changes[field].split('\n'):
366
        if not i:
367 368 369
            break
        s = i.split()
        section = priority = ""
J
sync  
James Troup 已提交
370
        try:
371
            if includes_section:
372
                (md5, size, section, priority, name) = s
373 374
            else:
                (md5, size, name) = s
J
sync  
James Troup 已提交
375
        except ValueError:
J
Joerg Jaspert 已提交
376
            raise ParseChangesError, i
J
James Troup 已提交
377

378
        if section == "":
379
            section = "-"
380
        if priority == "":
381
            priority = "-"
J
James Troup 已提交
382

383
        (section, component) = extract_component_from_section(section)
J
James Troup 已提交
384

385
        files[name] = Dict(size=size, section=section,
386
                           priority=priority, component=component)
387
        files[name][hashname] = md5
J
James Troup 已提交
388 389 390

    return files

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

393 394 395 396
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:
397 398
        unicode(s, 'utf-8')
        return s
399
    except UnicodeError:
400 401
        latin1_s = unicode(s,'iso8859-1')
        return latin1_s.encode('utf-8')
402 403 404 405 406

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:
407
        codecs.lookup('ascii')[1](s)
408
        return s
409
    except UnicodeError:
410
        pass
411
    try:
412
        codecs.lookup('utf-8')[1](s)
413 414
        h = email.Header.Header(s, 'utf-8', 998)
        return str(h)
415
    except UnicodeError:
416 417
        h = email.Header.Header(s, 'iso-8859-1', 998)
        return str(h)
418 419 420 421 422 423

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

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

J
James Troup 已提交
425
def fix_maintainer (maintainer):
426 427 428 429 430 431 432 433 434 435 436
    """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:
437
        return ('', '', '', '')
438

439
    if maintainer.find("<") == -1:
440 441
        email = maintainer
        name = ""
442
    elif (maintainer[0] == "<" and maintainer[-1:] == ">"):
443 444
        email = maintainer[1:-1]
        name = ""
445
    else:
446
        m = re_parse_maintainer.match(maintainer)
447 448
        if not m:
            raise ParseMaintError, "Doesn't parse as a valid Maintainer field."
449 450
        name = m.group(1)
        email = m.group(2)
451 452

    # Get an RFC2047 compliant version of the name
453
    rfc2047_name = rfc2047_encode(name)
454 455

    # Force the name to be UTF-8
456
    name = force_to_utf8(name)
457 458

    if name.find(',') != -1 or name.find('.') != -1:
459 460
        rfc822_maint = "%s (%s)" % (email, name)
        rfc2047_maint = "%s (%s)" % (email, rfc2047_name)
461
    else:
462 463
        rfc822_maint = "%s <%s>" % (name, email)
        rfc2047_maint = "%s <%s>" % (rfc2047_name, email)
464 465 466 467

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

468
    return (rfc822_maint, rfc2047_maint, name, email)
J
James Troup 已提交
469

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

# sendmail wrapper, takes _either_ a message string or a file as arguments
473
def send_mail (message, filename=""):
474 475 476 477 478 479 480 481 482 483
        # 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 已提交
484
        raise SendmailFailedError, output
485 486 487 488

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

490
################################################################################
J
James Troup 已提交
491 492

def poolify (source, component):
493
    if component:
494
        component += '/'
J
James Troup 已提交
495
    if source[:3] == "lib":
496
        return component + source[:4] + '/' + source + '/'
J
James Troup 已提交
497
    else:
498
        return component + source[:1] + '/' + source + '/'
J
James Troup 已提交
499

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

502
def move (src, dest, overwrite = 0, perms = 0664):
J
James Troup 已提交
503
    if os.path.exists(dest) and os.path.isdir(dest):
504
        dest_dir = dest
J
James Troup 已提交
505
    else:
506
        dest_dir = os.path.dirname(dest)
J
James Troup 已提交
507
    if not os.path.exists(dest_dir):
508 509 510
        umask = os.umask(00000)
        os.makedirs(dest_dir, 02775)
        os.umask(umask)
511
    #print "Moving %s to %s..." % (src, dest)
J
James Troup 已提交
512
    if os.path.exists(dest) and os.path.isdir(dest):
513
        dest += '/' + os.path.basename(src)
514 515 516
    # Don't overwrite unless forced to
    if os.path.exists(dest):
        if not overwrite:
517
            fubar("Can't move %s to %s - file already exists." % (src, dest))
518 519
        else:
            if not os.access(dest, os.W_OK):
520 521 522 523
                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 已提交
524

525
def copy (src, dest, overwrite = 0, perms = 0664):
J
James Troup 已提交
526
    if os.path.exists(dest) and os.path.isdir(dest):
527
        dest_dir = dest
J
James Troup 已提交
528
    else:
529
        dest_dir = os.path.dirname(dest)
J
James Troup 已提交
530
    if not os.path.exists(dest_dir):
531 532 533
        umask = os.umask(00000)
        os.makedirs(dest_dir, 02775)
        os.umask(umask)
534
    #print "Copying %s to %s..." % (src, dest)
J
James Troup 已提交
535
    if os.path.exists(dest) and os.path.isdir(dest):
536
        dest += '/' + os.path.basename(src)
537 538 539
    # Don't overwrite unless forced to
    if os.path.exists(dest):
        if not overwrite:
J
Joerg Jaspert 已提交
540
            raise FileExistsError
541 542
        else:
            if not os.access(dest, os.W_OK):
J
Joerg Jaspert 已提交
543
                raise CantOverwriteError
544 545
    shutil.copy2(src, dest)
    os.chmod(dest, perms)
J
James Troup 已提交
546

547
################################################################################
J
James Troup 已提交
548 549

def where_am_i ():
550 551
    res = socket.gethostbyaddr(socket.gethostname())
    database_hostname = Cnf.get("Config::" + res[0] + "::DatabaseHostname")
552
    if database_hostname:
553
        return database_hostname
J
James Troup 已提交
554
    else:
555
        return res[0]
J
James Troup 已提交
556 557

def which_conf_file ():
558
    res = socket.gethostbyaddr(socket.gethostname())
559
    if Cnf.get("Config::" + res[0] + "::DakConfig"):
560
        return Cnf["Config::" + res[0] + "::DakConfig"]
J
James Troup 已提交
561
    else:
562
        return default_config
563 564

def which_apt_conf_file ():
565
    res = socket.gethostbyaddr(socket.gethostname())
566
    if Cnf.get("Config::" + res[0] + "::AptConfig"):
567
        return Cnf["Config::" + res[0] + "::AptConfig"]
568
    else:
569
        return default_apt_config
570

T
Thomas Viehmann 已提交
571 572 573 574 575 576 577 578
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

579
################################################################################
J
James Troup 已提交
580

581 582 583 584
# Escape characters which have meaning to SQL's regex comparison operator ('~')
# (woefully incomplete)

def regex_safe (s):
585 586
    s = s.replace('+', '\\\\+')
    s = s.replace('.', '\\\\.')
587 588
    return s

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

J
James Troup 已提交
591
# Perform a substition of template
592
def TemplateSubst(map, filename):
593 594
    file = open_file(filename)
    template = file.read()
595
    for x in map.keys():
596 597 598
        template = template.replace(x,map[x])
    file.close()
    return template
J
James Troup 已提交
599

600
################################################################################
J
James Troup 已提交
601 602

def fubar(msg, exit_code=1):
603 604
    sys.stderr.write("E: %s\n" % (msg))
    sys.exit(exit_code)
J
James Troup 已提交
605 606

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

609
################################################################################
J
James Troup 已提交
610

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

616
################################################################################
J
James Troup 已提交
617

J
James Troup 已提交
618
def size_type (c):
619
    t  = " B"
620
    if c > 10240:
621 622
        c = c / 1024
        t = " KB"
623
    if c > 10240:
624 625
        c = c / 1024
        t = " MB"
J
James Troup 已提交
626
    return ("%d%s" % (c, t))
627 628 629 630

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

def cc_fix_changes (changes):
631
    o = changes.get("architecture", "")
632
    if o:
633 634
        del changes["architecture"]
    changes["architecture"] = {}
635
    for j in o.split():
636
        changes["architecture"][j] = 1
637

638
# Sort by source name, source version, 'have source', and then by filename
639 640
def changes_compare (a, b):
    try:
641
        a_changes = parse_changes(a)
642
    except:
643
        return -1
644 645

    try:
646
        b_changes = parse_changes(b)
647
    except:
648
        return 1
J
James Troup 已提交
649

650 651
    cc_fix_changes (a_changes)
    cc_fix_changes (b_changes)
652 653

    # Sort by source name
654 655 656
    a_source = a_changes.get("source")
    b_source = b_changes.get("source")
    q = cmp (a_source, b_source)
657
    if q:
658
        return q
659 660

    # Sort by source version
661 662 663
    a_version = a_changes.get("version", "0")
    b_version = b_changes.get("version", "0")
    q = apt_pkg.VersionCompare(a_version, b_version)
664
    if q:
665
        return q
666

667
    # Sort by 'have source'
668 669
    a_has_source = a_changes["architecture"].get("source")
    b_has_source = b_changes["architecture"].get("source")
670
    if a_has_source and not b_has_source:
671
        return -1
672
    elif b_has_source and not a_has_source:
673
        return 1
674

675
    # Fall back to sort by filename
676
    return cmp(a, b)
677 678

################################################################################
679 680

def find_next_free (dest, too_many=100):
681 682
    extra = 0
    orig_dest = dest
683
    while os.path.exists(dest) and extra < too_many:
684 685
        dest = orig_dest + '.' + repr(extra)
        extra += 1
686
    if extra >= too_many:
J
Joerg Jaspert 已提交
687
        raise NoFreeFilenameError
688
    return dest
J
James Troup 已提交
689

690
################################################################################
691 692

def result_join (original, sep = '\t'):
693
    list = []
694 695
    for i in xrange(len(original)):
        if original[i] == None:
696
            list.append("")
697
        else:
698 699
            list.append(original[i])
    return sep.join(list)
700

701 702
################################################################################

703
def prefix_multi_line_string(str, prefix, include_blank_lines=0):
704
    out = ""
705
    for line in str.split('\n'):
706
        line = line.strip()
707
        if line or include_blank_lines:
708
            out += "%s%s\n" % (prefix, line)
709 710
    # Strip trailing new line
    if out:
711 712
        out = out[:-1]
    return out
713

714
################################################################################
715

716
def validate_changes_file_arg(filename, require_changes=1):
717 718
    """'filename' is either a .changes or .dak file.  If 'filename' is a
.dak file, it's changed to be the corresponding .changes file.  The
719 720 721 722 723 724 725 726 727 728
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.
"""
729
    error = None
730

731
    orig_filename = filename
732
    if filename.endswith(".dak"):
733
        filename = filename[:-4]+".changes"
734

735
    if not filename.endswith(".changes"):
736
        error = "invalid file type; not a changes file"
737
    else:
738 739
        if not os.access(filename,os.R_OK):
            if os.path.exists(filename):
740
                error = "permission denied"
741
            else:
742
                error = "file not found"
743 744

    if error:
745
        if require_changes == 1:
746
            fubar("%s: %s." % (orig_filename, error))
747
        elif require_changes == 0:
748 749
            warn("Skipping %s - %s" % (orig_filename, error))
            return None
750
        else: # We only care about the .dak file
751
            return filename
752
    else:
753
        return filename
754 755 756

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

757
def real_arch(arch):
758
    return (arch != "source" and arch != "all")
759 760 761

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

762
def join_with_commas_and(list):
763 764 765
    if len(list) == 0: return "nothing"
    if len(list) == 1: return list[0]
    return ", ".join(list[:-1]) + " and " + list[-1]
766 767 768

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

769
def pp_deps (deps):
770
    pp_deps = []
771
    for atom in deps:
772
        (pkg, version, constraint) = atom
773
        if constraint:
774
            pp_dep = "%s (%s %s)" % (pkg, constraint, version)
775
        else:
776 777 778
            pp_dep = pkg
        pp_deps.append(pp_dep)
    return " |".join(pp_deps)
779 780 781

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

782
def get_conf():
783
    return Cnf
784 785 786

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

787 788 789 790
# Handle -a, -c and -s arguments; returns them as SQL constraints
def parse_args(Options):
    # Process suite
    if Options["Suite"]:
791
        suite_ids_list = []
792
        for suite in split_args(Options["Suite"]):
J
James Troup 已提交
793
            suite_id = database.get_suite_id(suite)
794
            if suite_id == -1:
795
                warn("suite '%s' not recognised." % (suite))
796
            else:
797
                suite_ids_list.append(suite_id)
798
        if suite_ids_list:
799
            con_suites = "AND su.id IN (%s)" % ", ".join([ str(i) for i in suite_ids_list ])
800
        else:
801
            fubar("No valid suite given.")
802
    else:
803
        con_suites = ""
804 805 806

    # Process component
    if Options["Component"]:
807
        component_ids_list = []
808
        for component in split_args(Options["Component"]):
J
James Troup 已提交
809
            component_id = database.get_component_id(component)
810
            if component_id == -1:
811
                warn("component '%s' not recognised." % (component))
812
            else:
813
                component_ids_list.append(component_id)
814
        if component_ids_list:
815
            con_components = "AND c.id IN (%s)" % ", ".join([ str(i) for i in component_ids_list ])
816
        else:
817
            fubar("No valid component given.")
818
    else:
819
        con_components = ""
820 821

    # Process architecture
822
    con_architectures = ""
823
    if Options["Architecture"]:
824 825
        arch_ids_list = []
        check_source = 0
826
        for architecture in split_args(Options["Architecture"]):
827
            if architecture == "source":
828
                check_source = 1
829
            else:
J
James Troup 已提交
830
                architecture_id = database.get_architecture_id(architecture)
831
                if architecture_id == -1:
832
                    warn("architecture '%s' not recognised." % (architecture))
833
                else:
834
                    arch_ids_list.append(architecture_id)
835
        if arch_ids_list:
836
            con_architectures = "AND a.id IN (%s)" % ", ".join([ str(i) for i in arch_ids_list ])
837 838
        else:
            if not check_source:
839
                fubar("No valid architecture given.")
840
    else:
841
        check_source = 1
842

843
    return (con_suites, con_architectures, con_components, check_source)
844 845 846

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

847 848 849 850
# Inspired(tm) by Bryn Keller's print_exc_plus (See
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52215)

def print_exc():
851
    tb = sys.exc_info()[2]
852
    while tb.tb_next:
853 854 855
        tb = tb.tb_next
    stack = []
    frame = tb.tb_frame
856
    while frame:
857 858 859 860
        stack.append(frame)
        frame = frame.f_back
    stack.reverse()
    traceback.print_exc()
861 862 863
    for frame in stack:
        print "\nFrame %s in %s at line %s" % (frame.f_code.co_name,
                                             frame.f_code.co_filename,
864
                                             frame.f_lineno)
865
        for key, value in frame.f_locals.items():
866
            print "\t%20s = " % key,
867
            try:
868
                print value
869
            except:
870
                print "<unable to print>"
871 872 873

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

J
James Troup 已提交
874 875
def try_with_debug(function):
    try:
876
        function()
J
James Troup 已提交
877
    except SystemExit:
878
        raise
J
James Troup 已提交
879
    except:
880
        print_exc()
J
James Troup 已提交
881 882 883

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

J
James Troup 已提交
884 885 886 887 888
# 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":
889
        return 0
J
James Troup 已提交
890
    elif a == "source":
891
        return -1
J
James Troup 已提交
892
    elif b == "source":
893
        return 1
J
James Troup 已提交
894

895
    return cmp (a, b)
J
James Troup 已提交
896 897 898

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

899 900
# Split command line arguments which can be separated by either commas
# or whitespace.  If dwim is set, it will complain about string ending
901
# in comma since this usually means someone did 'dak ls -a i386, m68k
902 903 904 905 906
# 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:
907
        return s.split()
908 909
    else:
        if s[-1:] == "," and dwim:
910 911
            fubar("split_args: found trailing comma, spurious space maybe?")
        return s.split(",")
912 913 914

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

915 916 917 918 919 920 921
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):
922 923 924 925 926
    cmd = ['/bin/sh', '-c', cmd]
    p2cread, p2cwrite = os.pipe()
    c2pread, c2pwrite = os.pipe()
    errout, errin = os.pipe()
    pid = os.fork()
927 928
    if pid == 0:
        # Child
929 930 931 932 933 934
        os.close(0)
        os.close(1)
        os.dup(p2cread)
        os.dup(c2pwrite)
        os.close(2)
        os.dup(errin)
935 936 937
        for i in range(3, 256):
            if i != status_write:
                try:
938
                    os.close(i)
939
                except:
940
                    pass
941
        try:
942
            os.execvp(cmd[0], cmd)
943
        finally:
944
            os._exit(1)
945

946
    # Parent
947
    os.close(p2cread)
948 949
    os.dup2(c2pread, c2pwrite)
    os.dup2(errout, errin)
950

951
    output = status = ""
952
    while 1:
953 954
        i, o, e = select.select([c2pwrite, errin, status_read], [], [])
        more_data = []
955
        for fd in i:
956
            r = os.read(fd, 8196)
957
            if len(r) > 0:
958
                more_data.append(fd)
959
                if fd == c2pwrite or fd == errin:
960
                    output += r
961
                elif fd == status_read:
962
                    status += r
963
                else:
964
                    fubar("Unexpected file descriptor [%s] returned from select\n" % (fd))
965 966 967
        if not more_data:
            pid, exit_status = os.waitpid(pid, 0)
            try:
968 969 970 971 972 973 974
                os.close(status_write)
                os.close(status_read)
                os.close(c2pread)
                os.close(c2pwrite)
                os.close(p2cwrite)
                os.close(errin)
                os.close(errout)
975
            except:
976 977
                pass
            break
978

979
    return output, status, exit_status
980

981
################################################################################
982

983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999
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:]
1000
        if keywords.has_key(keyword) and keyword not in [ "NODATA", "SIGEXPIRED", "KEYEXPIRED" ]:
1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018
            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:
1019
        keyring = Cnf.ValueList("Dinstall::GPGKeyring")[0]
1020 1021 1022 1023 1024 1025

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

1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053
    # 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 ""

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

1054 1055 1056 1057 1058 1059 1060 1061
def gpg_keyring_args(keyrings=None):
    if not keyrings:
        keyrings = Cnf.ValueList("Dinstall::GPGKeyring")

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

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

1062
def check_signature (sig_filename, reject, data_filename="", keyrings=None, autofetch=None):
1063 1064 1065 1066 1067
    """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,
1068
the second is an optional prefix string.  It's possible for reject()
1069 1070 1071
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
1072 1073 1074
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."""
1075 1076

    # Ensure the filename contains no shell meta-characters or other badness
1077
    if not re_taint_free.match(sig_filename):
1078 1079
        reject("!!WARNING!! tainted signature filename: '%s'." % (sig_filename))
        return None
1080 1081

    if data_filename and not re_taint_free.match(data_filename):
1082 1083
        reject("!!WARNING!! tainted data filename: '%s'." % (data_filename))
        return None
1084 1085

    if not keyrings:
1086
        keyrings = Cnf.ValueList("Dinstall::GPGKeyring")
1087

1088 1089 1090 1091 1092 1093 1094 1095 1096
    # 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

1097
    # Build the command line
1098
    status_read, status_write = os.pipe();
1099 1100 1101
    cmd = "gpgv --status-fd %s %s %s %s" % (
        status_write, gpg_keyring_args(keyrings), sig_filename, data_filename)

1102
    # Invoke gpgv on the file
1103
    (output, status, exit_status) = gpgv_get_status_output(cmd, status_read, status_write)
1104 1105

    # Process the status-fd output
1106
    (keywords, internal_error) = process_gpgv_output(status)
1107 1108 1109

    # If we failed to parse the status-fd output, let's just whine and bail now
    if internal_error:
1110 1111 1112 1113
        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
1114

1115
    bad = ""
1116 1117
    # Now check for obviously bad things in the processed output
    if keywords.has_key("KEYREVOKED"):
1118 1119
        reject("The key used to sign %s has been revoked." % (sig_filename))
        bad = 1
1120
    if keywords.has_key("BADSIG"):
1121 1122
        reject("bad signature on %s." % (sig_filename))
        bad = 1
1123
    if keywords.has_key("ERRSIG") and not keywords.has_key("NO_PUBKEY"):
1124 1125
        reject("failed to check signature on %s." % (sig_filename))
        bad = 1
1126
    if keywords.has_key("NO_PUBKEY"):
1127
        args = keywords["NO_PUBKEY"]
1128
        if len(args) >= 1:
1129 1130 1131
            key = args[0]
        reject("The key (0x%s) used to sign %s wasn't found in the keyring(s)." % (key, sig_filename))
        bad = 1
1132
    if keywords.has_key("BADARMOR"):
1133 1134
        reject("ASCII armour of signature was corrupt in %s." % (sig_filename))
        bad = 1
1135
    if keywords.has_key("NODATA"):
1136 1137
        reject("no signature found in %s." % (sig_filename))
        bad = 1
1138
    if keywords.has_key("KEYEXPIRED") and not keywords.has_key("GOODSIG"):
1139 1140 1141
        args = keywords["KEYEXPIRED"]
        if len(args) >= 1:
            key = args[0]
1142 1143
        reject("The key (0x%s) used to sign %s has expired." % (key, sig_filename))
        bad = 1
1144 1145

    if bad:
1146
        return None
1147 1148 1149

    # Next check gpgv exited with a zero return code
    if exit_status:
1150
        reject("gpgv failed while checking %s." % (sig_filename))
1151
        if status.strip():
1152
            reject(prefix_multi_line_string(status, " [GPG status-fd output:] "), "")
1153
        else:
1154 1155
            reject(prefix_multi_line_string(output, " [GPG output:] "), "")
        return None
1156 1157 1158

    # Sanity check the good stuff we expect
    if not keywords.has_key("VALIDSIG"):
1159 1160
        reject("signature on %s does not appear to be valid [No VALIDSIG]." % (sig_filename))
        bad = 1
1161
    else:
1162
        args = keywords["VALIDSIG"]
1163
        if len(args) < 1:
1164 1165
            reject("internal error while checking signature on %s." % (sig_filename))
            bad = 1
1166
        else:
1167
            fingerprint = args[0]
1168
    if not keywords.has_key("GOODSIG"):
1169 1170
        reject("signature on %s does not appear to be valid [No GOODSIG]." % (sig_filename))
        bad = 1
1171
    if not keywords.has_key("SIG_ID"):
1172 1173
        reject("signature on %s does not appear to be valid [No SIG_ID]." % (sig_filename))
        bad = 1
1174 1175 1176 1177

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

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

    if bad:
1186
        return None
1187
    else:
1188
        return fingerprint
1189 1190 1191

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

1192
def gpg_get_key_addresses(fingerprint):
1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207
    """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
1208 1209 1210

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

1211 1212 1213
# Inspired(tm) by http://www.zopelabs.com/cookbook/1022242603

def wrap(paragraph, max_length, prefix=""):
1214 1215 1216 1217
    line = ""
    s = ""
    have_started = 0
    words = paragraph.split()
1218 1219

    for word in words:
1220
        word_size = len(word)
1221 1222
        if word_size > max_length:
            if have_started:
1223 1224
                s += line + '\n' + prefix
            s += word + '\n' + prefix
1225 1226
        else:
            if have_started:
1227
                new_length = len(line) + word_size + 1
1228
                if new_length > max_length:
1229 1230
                    s += line + '\n' + prefix
                    line = word
1231
                else:
1232
                    line += ' ' + word
1233
            else:
1234 1235
                line = word
        have_started = 1
1236 1237

    if have_started:
1238
        s += line
1239

1240
    return s
1241 1242 1243 1244 1245 1246

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

# Relativize an absolute symlink from 'src' -> 'dest' relative to 'root'.
# Returns fixed 'src'
def clean_symlink (src, dest, root):
1247 1248 1249 1250 1251
    src = src.replace(root, '', 1)
    dest = dest.replace(root, '', 1)
    dest = os.path.dirname(dest)
    new_src = '../' * len(dest.split('/'))
    return new_src + src
1252 1253 1254

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

1255 1256 1257 1258 1259 1260
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:
1261 1262
        old_tempdir = tempfile.tempdir
        tempfile.tempdir = directory
1263

1264
    filename = tempfile.mktemp()
1265 1266

    if dotprefix:
1267 1268 1269
        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)
1270 1271

    if directory:
1272
        tempfile.tempdir = old_tempdir
1273

1274
    return filename
1275 1276 1277

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

T
Thomas Viehmann 已提交
1278 1279 1280
# checks if the user part of the email is listed in the alias file

def is_email_alias(email):
T
Thomas Viehmann 已提交
1281 1282 1283 1284 1285 1286 1287 1288 1289
    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 已提交
1290 1291 1292

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

1293
apt_pkg.init()
1294

1295 1296
Cnf = apt_pkg.newConfiguration()
apt_pkg.ReadConfigFileISC(Cnf,default_config)
1297 1298

if which_conf_file() != default_config:
1299
    apt_pkg.ReadConfigFileISC(Cnf,which_conf_file())
1300 1301

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