utils.py 36.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
26
import apt_pkg
J
James Troup 已提交
27
import database
28 29

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

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

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

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

45 46 47 48 49 50
changes_parse_error_exc = "Can't parse line in .changes file"
invalid_dsc_format_exc = "Invalid .dsc file"
nk_format_exc = "Unknown Format: in .changes file"
no_files_exc = "No Files: field in .dsc or .changes file."
cant_open_exc = "Can't open file"
unknown_hostname_exc = "Unknown hostname"
J
James Troup 已提交
51
cant_overwrite_exc = "Permission denied; can't overwrite existent file."
52 53 54
file_exists_exc = "Destination file exists"
sendmail_failed_exc = "Sendmail invocation failed"
tried_too_hard_exc = "Tried too hard to find a free filename."
55

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

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

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

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

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

    def __init__(self, message):
73 74
        self.args = message,
        self.message = message
75 76 77

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

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

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

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

98
################################################################################
J
James Troup 已提交
99

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

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

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

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

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

126
    return (section, component)
J
James Troup 已提交
127

128 129
################################################################################

130 131 132 133
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 已提交
134

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

151 152
    error = ""
    changes = {}
153

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

157
    if not lines:
158
	raise changes_parse_error_exc, "[Empty changes file]"
159

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

168
    inside_signature = 0
J
James Troup 已提交
169

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

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

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

226
    if error:
227
	raise changes_parse_error_exc, error
J
James Troup 已提交
228

229
    return changes
J
James Troup 已提交
230

231
################################################################################
J
James Troup 已提交
232 233 234

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

235
def build_file_list(changes, is_a_dsc=0):
236
    files = {}
237 238 239

    # Make sure we have a Files: field to parse...
    if not changes.has_key("files"):
240
	raise no_files_exc
241 242

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

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

263
        if section == "":
264
            section = "-"
265
        if priority == "":
266
            priority = "-"
J
James Troup 已提交
267

268
        (section, component) = extract_component_from_section(section)
J
James Troup 已提交
269

270
        files[name] = Dict(md5sum=md5, size=size, section=section,
271
                           priority=priority, component=component)
J
James Troup 已提交
272 273 274

    return files

275
################################################################################
J
James Troup 已提交
276

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

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

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

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

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

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

    # Get an RFC2047 compliant version of the name
337
    rfc2047_name = rfc2047_encode(name)
338 339

    # Force the name to be UTF-8
340
    name = force_to_utf8(name)
341 342

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

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

352
    return (rfc822_maint, rfc2047_maint, name, email)
J
James Troup 已提交
353

354
################################################################################
J
James Troup 已提交
355 356

# sendmail wrapper, takes _either_ a message string or a file as arguments
357
def send_mail (message, filename=""):
J
James Troup 已提交
358
	# If we've been passed a string dump it into a temporary file
359
	if message:
360 361 362 363
            filename = tempfile.mktemp()
            fd = os.open(filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0700)
            os.write (fd, message)
            os.close (fd)
J
James Troup 已提交
364

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

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

374
################################################################################
J
James Troup 已提交
375 376

def poolify (source, component):
377
    if component:
378
	component += '/'
J
James Troup 已提交
379
    # FIXME: this is nasty
380
    component = component.lower().replace("non-us/", "non-US/")
J
James Troup 已提交
381 382 383 384 385
    if source[:3] == "lib":
	return component + source[:4] + '/' + source + '/'
    else:
	return component + source[:1] + '/' + source + '/'

386
################################################################################
J
James Troup 已提交
387

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

411
def copy (src, dest, overwrite = 0, perms = 0664):
J
James Troup 已提交
412
    if os.path.exists(dest) and os.path.isdir(dest):
413
	dest_dir = dest
J
James Troup 已提交
414
    else:
415
	dest_dir = os.path.dirname(dest)
J
James Troup 已提交
416
    if not os.path.exists(dest_dir):
417 418 419 420
	umask = os.umask(00000)
	os.makedirs(dest_dir, 02775)
	os.umask(umask)
    #print "Copying %s to %s..." % (src, dest)
J
James Troup 已提交
421
    if os.path.exists(dest) and os.path.isdir(dest):
422
	dest += '/' + os.path.basename(src)
423 424 425 426 427 428 429
    # Don't overwrite unless forced to
    if os.path.exists(dest):
        if not overwrite:
            raise file_exists_exc
        else:
            if not os.access(dest, os.W_OK):
                raise cant_overwrite_exc
430 431
    shutil.copy2(src, dest)
    os.chmod(dest, perms)
J
James Troup 已提交
432

433
################################################################################
J
James Troup 已提交
434 435

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

def which_conf_file ():
444
    res = socket.gethostbyaddr(socket.gethostname())
445 446
    if Cnf.get("Config::" + res[0] + "::DakConfig"):
	return Cnf["Config::" + res[0] + "::DakConfig"]
J
James Troup 已提交
447
    else:
448
	return default_config
449 450

def which_apt_conf_file ():
451
    res = socket.gethostbyaddr(socket.gethostname())
452 453
    if Cnf.get("Config::" + res[0] + "::AptConfig"):
	return Cnf["Config::" + res[0] + "::AptConfig"]
454
    else:
455
	return default_apt_config
456

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

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

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

467
################################################################################
468

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

################################################################################
557 558

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

568
################################################################################
569 570

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

579 580
################################################################################

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

592
################################################################################
593

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

857
    return output, status, exit_status
858

859
################################################################################
860

861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906
def process_gpgv_output(status):
    # Process the status-fd output
    keywords = {}
    internal_error = ""
    for line in status.split('\n'):
        line = line.strip()
        if line == "":
            continue
        split = line.split()
        if len(split) < 2:
            internal_error += "gpgv status line is malformed (< 2 atoms) ['%s'].\n" % (line)
            continue
        (gnupg, keyword) = split[:2]
        if gnupg != "[GNUPG:]":
            internal_error += "gpgv status line is malformed (incorrect prefix '%s').\n" % (gnupg)
            continue
        args = split[2:]
        if keywords.has_key(keyword) and (keyword != "NODATA" and keyword != "SIGEXPIRED"):
            internal_error += "found duplicate status token ('%s').\n" % (keyword)
            continue
        else:
            keywords[keyword] = args

    return (keywords, internal_error)

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

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

    # Defaults for keyserver and keyring
    if not keyserver:
        keyserver = Cnf["Dinstall::KeyServer"]
    if not keyring:
        keyring = Cnf["Dinstall::GPGKeyring"]

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

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

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 ""

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

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

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

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

    if not keyrings:
        keyrings = (Cnf["Dinstall::PGPKeyring"], Cnf["Dinstall::GPGKeyring"])
957

958 959 960 961 962 963 964 965 966
    # 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

967 968
    # Build the command line
    status_read, status_write = os.pipe(); 
969
    cmd = "gpgv --status-fd %s" % (status_write)
970
    for keyring in keyrings:
971 972
        cmd += " --keyring %s" % (keyring)
    cmd += " %s %s" % (sig_filename, data_filename)
973
    # Invoke gpgv on the file
974
    (output, status, exit_status) = gpgv_get_status_output(cmd, status_read, status_write)
975 976

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

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

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

    if bad:
1014
        return None
1015 1016 1017

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

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

    # Finally ensure there's not something we don't recognise
    known_keywords = Dict(VALIDSIG="",SIG_ID="",GOODSIG="",BADSIG="",ERRSIG="",
                          SIGEXPIRED="",KEYREVOKED="",NO_PUBKEY="",BADARMOR="",
1046
                          NODATA="")
1047 1048 1049

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

    if bad:
1054
        return None
1055
    else:
1056
        return fingerprint
1057 1058 1059

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

1060 1061 1062
# Inspired(tm) by http://www.zopelabs.com/cookbook/1022242603

def wrap(paragraph, max_length, prefix=""):
1063 1064 1065 1066
    line = ""
    s = ""
    have_started = 0
    words = paragraph.split()
1067 1068

    for word in words:
1069
        word_size = len(word)
1070 1071
        if word_size > max_length:
            if have_started:
1072 1073
                s += line + '\n' + prefix
            s += word + '\n' + prefix
1074 1075
        else:
            if have_started:
1076
                new_length = len(line) + word_size + 1
1077
                if new_length > max_length:
1078 1079
                    s += line + '\n' + prefix
                    line = word
1080
                else:
1081
                    line += ' ' + word
1082
            else:
1083 1084
                line = word
        have_started = 1
1085 1086

    if have_started:
1087
        s += line
1088

1089
    return s
1090 1091 1092 1093 1094 1095

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

# Relativize an absolute symlink from 'src' -> 'dest' relative to 'root'.
# Returns fixed 'src'
def clean_symlink (src, dest, root):
1096 1097 1098 1099 1100
    src = src.replace(root, '', 1)
    dest = dest.replace(root, '', 1)
    dest = os.path.dirname(dest)
    new_src = '../' * len(dest.split('/'))
    return new_src + src
1101 1102 1103

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

1104 1105 1106 1107 1108 1109
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:
1110 1111
        old_tempdir = tempfile.tempdir
        tempfile.tempdir = directory
1112

1113
    filename = tempfile.mktemp()
1114 1115

    if dotprefix:
1116 1117 1118
        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)
1119 1120

    if directory:
1121
        tempfile.tempdir = old_tempdir
1122

1123
    return filename
1124 1125 1126

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

1127
apt_pkg.init()
1128

1129 1130
Cnf = apt_pkg.newConfiguration()
apt_pkg.ReadConfigFileISC(Cnf,default_config)
1131 1132

if which_conf_file() != default_config:
1133
	apt_pkg.ReadConfigFileISC(Cnf,which_conf_file())
1134 1135

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