utils.py 30.1 KB
Newer Older
1 2
#!/usr/bin/env python

J
James Troup 已提交
3
# Utility functions
4
# Copyright (C) 2000, 2001, 2002, 2003  James Troup <james@nocrew.org>
5
# $Id: utils.py,v 1.59 2003-09-24 00:13:43 troup Exp $
6 7

################################################################################
J
James Troup 已提交
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

# 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

23 24
################################################################################

25
import commands, os, pwd, re, select, socket, shutil, string, sys, tempfile, traceback;
26 27 28 29
import apt_pkg;
import db_access;

################################################################################
J
James Troup 已提交
30 31 32 33 34 35

re_comments = re.compile(r"\#.*")
re_no_epoch = re.compile(r"^\d*\:")
re_no_revision = re.compile(r"\-[^-]*$")
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

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

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

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

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

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

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

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

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

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

83 84 85 86 87 88
def str_isnum (s):
    for c in s:
        if c not in string.digits:
            return 0;
    return 1;

89
################################################################################
90

J
James Troup 已提交
91 92
def extract_component_from_section(section):
    component = "";
J
James Troup 已提交
93

94 95 96 97
    if section.find('/') != -1:
        component = section.split('/')[0];
    if component.lower() == "non-us" and section.count('/') > 0:
        s = component + '/' + section.split('/')[1];
98
        if Cnf.has_key("Component::%s" % s): # Avoid e.g. non-US/libs
J
James Troup 已提交
99
            component = s;
J
James Troup 已提交
100

101
    if section.lower() == "non-us":
J
James Troup 已提交
102
        component = "non-US/main";
103 104

    # non-US prefix is case insensitive
105
    if component.lower()[:6] == "non-us":
106 107 108
        component = "non-US"+component[6:];

    # Expand default component
J
James Troup 已提交
109
    if component == "":
110
        if Cnf.has_key("Component::%s" % section):
111 112 113 114
            component = section;
        else:
            component = "main";
    elif component == "non-US":
J
James Troup 已提交
115 116 117 118
        component = "non-US/main";

    return (section, component);

119 120 121 122 123
################################################################################

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

125 126
# dsc_whitespace_rules is an optional boolean argument which defaults
# to off.  If true, it turns on strict format checking to avoid
J
James Troup 已提交
127 128 129 130 131
# allowing in source packages which are unextracable by the
# inappropriately fragile dpkg-source.
#
# The rules are:
#
132 133
#   o The PGP header consists of "-----BEGIN PGP SIGNED MESSAGE-----"
#     followed by any PGP header data and must end with a blank line.
J
James Troup 已提交
134
#
135 136
#   o The data section must end with a blank line and must be followed by
#     "-----BEGIN PGP SIGNATURE-----".
J
James Troup 已提交
137

138
def parse_changes(filename, dsc_whitespace_rules=0):
J
James Troup 已提交
139
    error = "";
J
James Troup 已提交
140
    changes = {};
141 142

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

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

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

    inside_signature = 0;

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

    if dsc_whitespace_rules and inside_signature:
        raise invalid_dsc_format_exc, index;
J
James Troup 已提交
207

J
James Troup 已提交
208
    changes_in.close();
209
    changes["filecontents"] = "".join(lines);
J
James Troup 已提交
210

211
    if error:
J
James Troup 已提交
212
	raise changes_parse_error_exc, error;
J
James Troup 已提交
213

J
James Troup 已提交
214 215
    return changes;

216
################################################################################
J
James Troup 已提交
217 218 219

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

220
def build_file_list(changes, is_a_dsc=0):
221 222 223 224 225 226 227 228
    files = {};

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

    # Make sure we recognise the format of the Files: field
    format = changes.get("format", "");
J
James Troup 已提交
229
    if format != "":
230
	format = float(format);
231
    if not is_a_dsc and (format < 1.5 or format > 2.0):
232
	raise nk_format_exc, format;
J
James Troup 已提交
233

234 235 236 237
    # Parse each entry/line:
    for i in changes["files"].split('\n'):
        if not i:
            break;
238
        s = i.split();
J
James Troup 已提交
239
        section = priority = "";
J
sync  
James Troup 已提交
240
        try:
241
            if is_a_dsc:
242
                (md5, size, name) = s;
J
sync  
James Troup 已提交
243
            else:
244
                (md5, size, section, priority, name) = s;
J
sync  
James Troup 已提交
245
        except ValueError:
246
            raise changes_parse_error_exc, i;
J
James Troup 已提交
247

248 249 250 251
        if section == "":
            section = "-";
        if priority == "":
            priority = "-";
J
James Troup 已提交
252

J
James Troup 已提交
253
        (section, component) = extract_component_from_section(section);
J
James Troup 已提交
254

255 256
        files[name] = Dict(md5sum=md5, size=size, section=section,
                           priority=priority, component=component);
J
James Troup 已提交
257 258 259

    return files

260
################################################################################
J
James Troup 已提交
261 262

# Fix the `Maintainer:' field to be an RFC822 compatible address.
263
# cf. Debian Policy Manual (D.2.4)
J
James Troup 已提交
264 265 266 267
#
# 06:28|<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 已提交
268

J
James Troup 已提交
269
def fix_maintainer (maintainer):
270
    m = re_parse_maintainer.match(maintainer);
271 272 273
    rfc822 = maintainer;
    name = "";
    email = "";
J
James Troup 已提交
274
    if m != None and len(m.groups()) == 2:
275 276
        name = m.group(1);
        email = m.group(2);
277
        if name.find(',') != -1 or name.find('.') != -1:
278
            rfc822 = "%s (%s)" % (email, name);
J
James Troup 已提交
279 280
    return (rfc822, name, email)

281
################################################################################
J
James Troup 已提交
282 283

# sendmail wrapper, takes _either_ a message string or a file as arguments
284
def send_mail (message, filename=""):
J
James Troup 已提交
285
	# If we've been passed a string dump it into a temporary file
286
	if message:
J
James Troup 已提交
287 288 289 290 291
            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 已提交
292
	# Invoke sendmail
293
	(result, output) = commands.getstatusoutput("%s < %s" % (Cnf["Dinstall::SendmailCommand"], filename));
J
James Troup 已提交
294
	if (result != 0):
J
James Troup 已提交
295 296
            raise sendmail_failed_exc, output;

J
James Troup 已提交
297
	# Clean up any temporary files
298
	if message:
J
James Troup 已提交
299
            os.unlink (filename);
J
James Troup 已提交
300

301
################################################################################
J
James Troup 已提交
302 303

def poolify (source, component):
304
    if component:
305
	component += '/';
J
James Troup 已提交
306
    # FIXME: this is nasty
307
    component = component.lower().replace("non-us/", "non-US/");
J
James Troup 已提交
308 309 310 311 312
    if source[:3] == "lib":
	return component + source[:4] + '/' + source + '/'
    else:
	return component + source[:1] + '/' + source + '/'

313
################################################################################
J
James Troup 已提交
314

315
def move (src, dest, overwrite = 0, perms = 0664):
J
James Troup 已提交
316
    if os.path.exists(dest) and os.path.isdir(dest):
J
James Troup 已提交
317 318 319 320 321 322 323 324
	dest_dir = dest;
    else:
	dest_dir = os.path.dirname(dest);
    if not os.path.exists(dest_dir):
	umask = os.umask(00000);
	os.makedirs(dest_dir, 02775);
	os.umask(umask);
    #print "Moving %s to %s..." % (src, dest);
J
James Troup 已提交
325
    if os.path.exists(dest) and os.path.isdir(dest):
326
	dest += '/' + os.path.basename(src);
327 328 329
    # Don't overwrite unless forced to
    if os.path.exists(dest):
        if not overwrite:
330
            fubar("Can't move %s to %s - file already exists." % (src, dest));
331 332
        else:
            if not os.access(dest, os.W_OK):
333
                fubar("Can't move %s to %s - can't write to existing file." % (src, dest));
J
James Troup 已提交
334
    shutil.copy2(src, dest);
335
    os.chmod(dest, perms);
J
James Troup 已提交
336 337
    os.unlink(src);

338
def copy (src, dest, overwrite = 0, perms = 0664):
J
James Troup 已提交
339
    if os.path.exists(dest) and os.path.isdir(dest):
J
James Troup 已提交
340 341 342 343 344 345 346 347
	dest_dir = dest;
    else:
	dest_dir = os.path.dirname(dest);
    if not os.path.exists(dest_dir):
	umask = os.umask(00000);
	os.makedirs(dest_dir, 02775);
	os.umask(umask);
    #print "Copying %s to %s..." % (src, dest);
J
James Troup 已提交
348
    if os.path.exists(dest) and os.path.isdir(dest):
349
	dest += '/' + os.path.basename(src);
350 351 352 353 354 355 356
    # 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
J
James Troup 已提交
357
    shutil.copy2(src, dest);
358
    os.chmod(dest, perms);
J
James Troup 已提交
359

360
################################################################################
J
James Troup 已提交
361 362 363

def where_am_i ():
    res = socket.gethostbyaddr(socket.gethostname());
364
    database_hostname = Cnf.get("Config::" + res[0] + "::DatabaseHostname");
365 366
    if database_hostname:
	return database_hostname;
J
James Troup 已提交
367
    else:
J
James Troup 已提交
368
        return res[0];
J
James Troup 已提交
369 370

def which_conf_file ():
J
James Troup 已提交
371
    res = socket.gethostbyaddr(socket.gethostname());
372 373
    if Cnf.get("Config::" + res[0] + "::KatieConfig"):
	return Cnf["Config::" + res[0] + "::KatieConfig"]
J
James Troup 已提交
374
    else:
J
James Troup 已提交
375
	return default_config;
376 377

def which_apt_conf_file ():
J
James Troup 已提交
378
    res = socket.gethostbyaddr(socket.gethostname());
379 380
    if Cnf.get("Config::" + res[0] + "::AptConfig"):
	return Cnf["Config::" + res[0] + "::AptConfig"]
381
    else:
J
James Troup 已提交
382
	return default_apt_config;
383

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

386 387 388 389
# Escape characters which have meaning to SQL's regex comparison operator ('~')
# (woefully incomplete)

def regex_safe (s):
390 391
    s = s.replace('+', '\\\\+');
    s = s.replace('.', '\\\\.');
392 393
    return s

394
################################################################################
395

J
James Troup 已提交
396
# Perform a substition of template
397
def TemplateSubst(map, filename):
398 399 400
    file = open_file(filename);
    template = file.read();
    for x in map.keys():
401
        template = template.replace(x,map[x]);
402 403
    file.close();
    return template;
J
James Troup 已提交
404

405
################################################################################
J
James Troup 已提交
406 407 408 409 410 411 412

def fubar(msg, exit_code=1):
    sys.stderr.write("E: %s\n" % (msg));
    sys.exit(exit_code);

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

414
################################################################################
J
James Troup 已提交
415

J
James Troup 已提交
416 417 418
# Returns the user name with a laughable attempt at rfc822 conformancy
# (read: removing stray periods).
def whoami ():
419
    return pwd.getpwuid(os.getuid())[4].split(',')[0].replace('.', '');
J
James Troup 已提交
420

421
################################################################################
J
James Troup 已提交
422

J
James Troup 已提交
423 424 425 426 427 428 429 430 431
def size_type (c):
    t  = " b";
    if c > 10000:
        c = c / 1000;
        t = " Kb";
    if c > 10000:
        c = c / 1000;
        t = " Mb";
    return ("%d%s" % (c, t))
432 433 434 435

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

def cc_fix_changes (changes):
436 437 438 439
    o = changes.get("architecture", "");
    if o:
        del changes["architecture"];
    changes["architecture"] = {};
440
    for j in o.split():
441
        changes["architecture"][j] = 1;
442

443
# Sort by source name, source version, 'have source', and then by filename
444 445
def changes_compare (a, b):
    try:
446
        a_changes = parse_changes(a);
447
    except:
448 449 450
        return -1;

    try:
451
        b_changes = parse_changes(b);
452
    except:
453
        return 1;
J
James Troup 已提交
454

455 456 457 458 459 460 461 462 463 464 465 466 467 468 469
    cc_fix_changes (a_changes);
    cc_fix_changes (b_changes);

    # Sort by source name
    a_source = a_changes.get("source");
    b_source = b_changes.get("source");
    q = cmp (a_source, b_source);
    if q:
        return q;

    # Sort by source version
    a_version = a_changes.get("version");
    b_version = b_changes.get("version");
    q = apt_pkg.VersionCompare(a_version, b_version);
    if q:
470
        return q;
471

472
    # Sort by 'have source'
473 474
    a_has_source = a_changes["architecture"].get("source");
    b_has_source = b_changes["architecture"].get("source");
475 476 477 478
    if a_has_source and not b_has_source:
        return -1;
    elif b_has_source and not a_has_source:
        return 1;
479

480
    # Fall back to sort by filename
481 482 483
    return cmp(a, b);

################################################################################
484 485 486 487 488 489

def find_next_free (dest, too_many=100):
    extra = 0;
    orig_dest = dest;
    while os.path.exists(dest) and extra < too_many:
        dest = orig_dest + '.' + repr(extra);
490
        extra += 1;
491 492 493
    if extra >= too_many:
        raise tried_too_hard_exc;
    return dest;
J
James Troup 已提交
494

495
################################################################################
496 497 498 499 500 501 502 503

def result_join (original, sep = '\t'):
    list = [];
    for i in xrange(len(original)):
        if original[i] == None:
            list.append("");
        else:
            list.append(original[i]);
504
    return sep.join(list);
505

506 507
################################################################################

508
def prefix_multi_line_string(str, prefix, include_blank_lines=0):
509
    out = "";
510 511
    for line in str.split('\n'):
        line = line.strip();
512
        if line or include_blank_lines:
513
            out += "%s%s\n" % (prefix, line);
514 515 516 517
    # Strip trailing new line
    if out:
        out = out[:-1];
    return out;
518

519
################################################################################
520

521 522 523
def validate_changes_file_arg(file, fatal=1):
    error = None;

524
    orig_filename = file
525
    if file.endswith(".katie"):
526 527
        file = file[:-6]+".changes";

528
    if not file.endswith(".changes"):
529 530 531 532 533 534 535 536 537 538
        error = "invalid file type; not a changes file";
    else:
        if not os.access(file,os.R_OK):
            if os.path.exists(file):
                error = "permission denied";
            else:
                error = "file not found";

    if error:
        if fatal:
539
            fubar("%s: %s." % (orig_filename, error));
540
        else:
541
            warn("Skipping %s - %s" % (orig_filename, error));
542 543 544 545 546 547
            return None;
    else:
        return file;

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

548 549 550 551 552
def real_arch(arch):
    return (arch != "source" and arch != "all");

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

553 554 555
def join_with_commas_and(list):
	if len(list) == 0: return "nothing";
	if len(list) == 1: return list[0];
556
	return ", ".join(list[:-1]) + " and " + list[-1];
557 558 559

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

560 561 562 563 564
def get_conf():
	return Cnf;

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

565 566 567 568 569
# Handle -a, -c and -s arguments; returns them as SQL constraints
def parse_args(Options):
    # Process suite
    if Options["Suite"]:
        suite_ids_list = [];
570
        for suite in split_args(Options["Suite"]):
571 572
            suite_id = db_access.get_suite_id(suite);
            if suite_id == -1:
573
                warn("suite '%s' not recognised." % (suite));
574 575 576
            else:
                suite_ids_list.append(suite_id);
        if suite_ids_list:
577
            con_suites = "AND su.id IN (%s)" % ", ".join(map(str, suite_ids_list));
578 579 580 581 582 583 584 585
        else:
            fubar("No valid suite given.");
    else:
        con_suites = "";

    # Process component
    if Options["Component"]:
        component_ids_list = [];
586
        for component in split_args(Options["Component"]):
587 588 589 590 591 592
            component_id = db_access.get_component_id(component);
            if component_id == -1:
                warn("component '%s' not recognised." % (component));
            else:
                component_ids_list.append(component_id);
        if component_ids_list:
593
            con_components = "AND c.id IN (%s)" % ", ".join(map(str, component_ids_list));
594 595 596 597 598 599 600 601 602 603
        else:
            fubar("No valid component given.");
    else:
        con_components = "";

    # Process architecture
    con_architectures = "";
    if Options["Architecture"]:
        arch_ids_list = [];
        check_source = 0;
604
        for architecture in split_args(Options["Architecture"]):
605 606 607 608 609 610 611 612 613
            if architecture == "source":
                check_source = 1;
            else:
                architecture_id = db_access.get_architecture_id(architecture);
                if architecture_id == -1:
                    warn("architecture '%s' not recognised." % (architecture));
                else:
                    arch_ids_list.append(architecture_id);
        if arch_ids_list:
614
            con_architectures = "AND a.id IN (%s)" % ", ".join(map(str, arch_ids_list));
615 616 617 618 619 620 621 622 623 624
        else:
            if not check_source:
                fubar("No valid architecture given.");
    else:
        check_source = 1;

    return (con_suites, con_architectures, con_components, check_source);

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

625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651
# Inspired(tm) by Bryn Keller's print_exc_plus (See
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52215)

def print_exc():
    tb = sys.exc_info()[2];
    while tb.tb_next:
        tb = tb.tb_next;
    stack = [];
    frame = tb.tb_frame;
    while frame:
        stack.append(frame);
        frame = frame.f_back;
    stack.reverse();
    traceback.print_exc();
    for frame in stack:
        print "\nFrame %s in %s at line %s" % (frame.f_code.co_name,
                                             frame.f_code.co_filename,
                                             frame.f_lineno);
        for key, value in frame.f_locals.items():
            print "\t%20s = " % key,;
            try:
                print value;
            except:
                print "<unable to print>";

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

J
James Troup 已提交
652 653 654 655 656 657 658 659 660 661
def try_with_debug(function):
    try:
        function();
    except SystemExit:
        raise;
    except:
        print_exc();

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

J
James Troup 已提交
662 663 664 665 666 667 668 669 670 671 672 673 674 675 676
# 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":
        return 0;
    elif a == "source":
        return -1;
    elif b == "source":
        return 1;

    return cmp (a, b);

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

677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692
# Split command line arguments which can be separated by either commas
# or whitespace.  If dwim is set, it will complain about string ending
# in comma since this usually means someone did 'madison -a i386, m68k
# 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:
        return s.split();
    else:
        if s[-1:] == "," and dwim:
            fubar("split_args: found trailing comma, spurious space maybe?");
        return s.split(",");

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

693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723
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):
    cmd = ['/bin/sh', '-c', cmd];
    p2cread, p2cwrite = os.pipe();
    c2pread, c2pwrite = os.pipe();
    errout, errin = os.pipe();
    pid = os.fork();
    if pid == 0:
        # Child
        os.close(0);
        os.close(1);
        os.dup(p2cread);
        os.dup(c2pwrite);
        os.close(2);
        os.dup(errin);
        for i in range(3, 256):
            if i != status_write:
                try:
                    os.close(i);
                except:
                    pass;
        try:
            os.execvp(cmd[0], cmd);
        finally:
            os._exit(1);

724
    # Parent
725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767
    os.close(p2cread)
    os.dup2(c2pread, c2pwrite);
    os.dup2(errout, errin);

    output = status = "";
    while 1:
        i, o, e = select.select([c2pwrite, errin, status_read], [], []);
        more_data = [];
        for fd in i:
            r = os.read(fd, 8196);
            if len(r) > 0:
                more_data.append(fd);
                if fd == c2pwrite or fd == errin:
                    output += r;
                elif fd == status_read:
                    status += r;
                else:
                    fubar("Unexpected file descriptor [%s] returned from select\n" % (fd));
        if not more_data:
            pid, exit_status = os.waitpid(pid, 0)
            try:
                os.close(status_write);
                os.close(status_read);
                os.close(c2pread);
                os.close(c2pwrite);
                os.close(p2cwrite);
                os.close(errin);
                os.close(errout);
            except:
                pass;
            break;

    return output, status, exit_status;

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


def check_signature (filename, reject):
    """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,
768 769
the second is an optional prefix string.  It's possible for reject()
to be called more than once during an invocation of check_signature()."""
770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812

    # Ensure the filename contains no shell meta-characters or other badness
    if not re_taint_free.match(os.path.basename(filename)):
        reject("!!WARNING!! tainted filename: '%s'." % (filename));
        return 0;

    # Invoke gpgv on the file
    status_read, status_write = os.pipe();
    cmd = "gpgv --status-fd %s --keyring %s --keyring %s %s" \
          % (status_write, Cnf["Dinstall::PGPKeyring"], Cnf["Dinstall::GPGKeyring"], filename);
    (output, status, exit_status) = gpgv_get_status_output(cmd, status_read, status_write);

    # Process the status-fd output
    keywords = {};
    bad = 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;

    # If we failed to parse the status-fd output, let's just whine and bail now
    if internal_error:
        reject("internal error while performing signature check on %s." % (filename));
        reject(internal_error, "");
        reject("Please report the above errors to the Archive maintainers by replying to this mail.", "");
        return None;

    # Now check for obviously bad things in the processed output
    if keywords.has_key("SIGEXPIRED"):
813
        reject("The key used to sign %s has expired." % (filename));
814 815
        bad = 1;
    if keywords.has_key("KEYREVOKED"):
816
        reject("The key used to sign %s has been revoked." % (filename));
817 818 819 820 821 822 823 824
        bad = 1;
    if keywords.has_key("BADSIG"):
        reject("bad signature on %s." % (filename));
        bad = 1;
    if keywords.has_key("ERRSIG") and not keywords.has_key("NO_PUBKEY"):
        reject("failed to check signature on %s." % (filename));
        bad = 1;
    if keywords.has_key("NO_PUBKEY"):
825 826 827 828
        args = keywords["NO_PUBKEY"];
        if len(args) >= 1:
            key = args[0];
        reject("The key (0x%s) used to sign %s wasn't found in the keyring(s)." % (key, filename));
829 830
        bad = 1;
    if keywords.has_key("BADARMOR"):
831
        reject("ASCII armour of signature was corrupt in %s." % (filename));
832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 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
        bad = 1;
    if keywords.has_key("NODATA"):
        reject("no signature found in %s." % (filename));
        bad = 1;

    if bad:
        return None;

    # Next check gpgv exited with a zero return code
    if exit_status:
        reject("gpgv failed while checking %s." % (filename));
        if status.strip():
            reject(prefix_multi_line_string(status, " [GPG status-fd output:] "), "");
        else:
            reject(prefix_multi_line_string(output, " [GPG output:] "), "");
        return None;

    # Sanity check the good stuff we expect
    if not keywords.has_key("VALIDSIG"):
        reject("signature on %s does not appear to be valid [No VALIDSIG]." % (filename));
        bad = 1;
    else:
        args = keywords["VALIDSIG"];
        if len(args) < 1:
            reject("internal error while checking signature on %s." % (filename));
            bad = 1;
        else:
            fingerprint = args[0];
    if not keywords.has_key("GOODSIG"):
        reject("signature on %s does not appear to be valid [No GOODSIG]." % (filename));
        bad = 1;
    if not keywords.has_key("SIG_ID"):
        reject("signature on %s does not appear to be valid [No SIG_ID]." % (filename));
        bad = 1;

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

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

    if bad:
        return None;
    else:
        return fingerprint;

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

884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928
# Inspired(tm) by http://www.zopelabs.com/cookbook/1022242603

def wrap(paragraph, max_length, prefix=""):
    line = "";
    s = "";
    have_started = 0;
    words = paragraph.split();

    for word in words:
        word_size = len(word);
        if word_size > max_length:
            if have_started:
                s += line + '\n' + prefix;
            s += word + '\n' + prefix;
        else:
            if have_started:
                new_length = len(line) + word_size + 1;
                if new_length > max_length:
                    s += line + '\n' + prefix;
                    line = word;
                else:
                    line += ' ' + word;
            else:
                line = word;
        have_started = 1;

    if have_started:
        s += line;

    return s;

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

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

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

apt_pkg.init();
929 930 931 932 933

Cnf = apt_pkg.newConfiguration();
apt_pkg.ReadConfigFileISC(Cnf,default_config);

if which_conf_file() != default_config:
934
	apt_pkg.ReadConfigFileISC(Cnf,which_conf_file());
935 936

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