Subversion Repositories

?revision_form?Rev ?revision_input??revision_submit??revision_endform?

Rev 9 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
1 magnus 1
package DebPool::Packages;
2
 
3
###
4
#
5
# DebPool::Packages - Module for handling package metadata
6
#
7
# Copyright 2003-2004 Joel Aelwyn. All rights reserved.
8
# 
9
# Redistribution and use in source and binary forms, with or without
10
# modification, are permitted provided that the following conditions
11
# are met:
12
# 1. Redistributions of source code must retain the above copyright
13
#    notice, this list of conditions and the following disclaimer.
14
# 2. Redistributions in binary form must reproduce the above copyright
15
#    notice, this list of conditions and the following disclaimer in the
16
#    documentation and/or other materials provided with the distribution.
17
# 3. Neither the name of the Author nor the names of any contributors
18
#    may be used to endorse or promote products derived from this software
19
#    without specific prior written permission.
20
# 
21
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
22
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
25
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
27
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
28
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
30
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
31
# SUCH DAMAGE.
32
#
33
# $Id: Packages.pm 70 2006-06-26 20:44:57Z joel $
34
#
35
###
36
 
37
# We use 'our', so we must have at least Perl 5.6
38
 
39
require 5.006_000;
40
 
41
# Always good ideas.
42
 
43
use strict;
44
use warnings;
45
 
46
use POSIX; # WEXITSTATUS
47
use File::Temp qw(tempfile);
48
 
49
### Module setup
50
 
51
BEGIN {
52
    use Exporter ();
53
    our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
54
 
55
    # Version checking
56
    $VERSION = '0.1.5';
57
 
58
    @ISA = qw(Exporter);
59
 
60
    @EXPORT = qw(
61
    );
62
 
63
    @EXPORT_OK = qw(
64
        &Allow_Version
65
        &Audit_Package
66
        &Generate_List
67
        &Generate_Package
68
        &Generate_Source
69
        &Guess_Section
70
        &Install_List
71
        &Install_Package
72
        &Parse_Changes
73
        &Parse_DSC
74
        &Reject_Package
75
        &Verify_MD5
76
    );
77
 
78
    %EXPORT_TAGS = (
79
        'functions' => [qw(&Allow_Version &Audit_Package &Generate_List
80
                           &Generate_Package &Generate_Source &Guess_Section
81
                           &Install_List &Install_Package &Parse_Changes
82
                           &Parse_DSC &Reject_Package &Verify_MD5)],
83
        'vars' => [qw()],
84
    );
85
}
86
 
87
### Exported package globals
88
 
89
# None
90
 
91
### Non-exported package globals
92
 
93
# Thread-safe? What's that? Package global error value. We don't export
94
# this directly, because it would conflict with other modules.
95
 
96
our($Error);
97
 
98
# Fields (other than package relationships) from dpkg --info that we
99
# actually care about in some fashion.
100
 
101
my(@Info_Fields) = (
102
#    'Package',
103
    'Priority',
104
    'Section',
105
    'Installed-Size',
106
#    'Maintainer',
107
    'Architecture',
108
#    'Version',
109
    'Essential',
110
);
111
 
112
# Package relationship fieldnames.
113
 
114
my(@Relationship_Fields) = (
115
    'Pre-Depends',
116
    'Depends',
117
    'Provides',
118
    'Conflicts',
119
    'Recommends',
120
    'Suggests',
121
    'Enhances',
122
    'Replaces',
123
);
124
 
125
# Normal fields potentially found in .changes files
126
 
127
my(%Changes_Fields) = (
128
    'Format' => 'string',
129
    'Date' => 'string',
130
    'Source' => 'string',
131
    'Binary' => 'space_array',
132
    'Architecture' => 'space_array',
133
    'Version' => 'string',
134
    'Distribution' => 'space_array',
135
    'Urgency' => 'string',
136
    'Maintainer' => 'string',
137
    'Changed-By' => 'string',
138
    'Closes' => 'space_array',
139
);
140
 
141
# Normal fields potentially found in .dsc files
142
 
143
my(%DSC_Fields) = (
144
    'Format' => 'string',
145
    'Source' => 'string',
146
    'Version' => 'string',
147
    'Binary' => 'comma_array',
148
    'Maintainer' => 'string',
149
    'Architecture' => 'space_array',
150
    'Standards-Version' => 'string',
151
    'Build-Depends' => 'comma_array',
152
    'Build-Depends-Indep' => 'comma_array',
153
);
154
 
155
### File lexicals
156
 
157
# None
158
 
159
### Constant functions
160
 
161
# None
162
 
163
### Meaningful functions
164
 
165
# Allow_Version($package, $version, $distribution)
166
#
167
# Decide, based on version comparison and config options, whether $version
168
# is an acceptable version for $package in $distribution. Returns 1 if the
169
# version is acceptable, 0 if it is not, and undef (and sets $Error) in the
170
# case of an error.
171
 
172
sub Allow_Version {
173
    use DebPool::Config qw(:vars);
174
    use DebPool::DB qw(:functions);
175
    use DebPool::Logging qw(:functions :facility :level);
176
 
177
    my($package, $version, $distribution) = @_;
178
    my($old_version) = Get_Version($distribution, $package, 'meta');
179
 
180
    # If we permit rollback, any version is valid.
181
 
182
    if ($Options{'rollback'}) {
183
        return 1;
184
    }
185
 
186
    # If we don't have an old version, anything is acceptable.
187
 
188
    if (!defined($old_version)) {
189
        return 1;
190
    }
191
 
192
    my($dpkg_bin) = '/usr/bin/dpkg';
193
    my(@args) = ('--compare-versions', $version, 'gt', $old_version);
194
 
195
    my($sysret) = WEXITSTATUS(system($dpkg_bin, @args));
196
 
197
    if (0 != $sysret) { # DPKG says no go.
198
        my($msg) = "Version comparison for '$package': proposed version for ";
199
        $msg .= "$distribution ($version) is not greater than current ";
200
        $msg .= "version ($old_version)";
201
        Log_Message($msg, LOG_GENERAL, LOG_DEBUG);
202
 
203
        return 0;
204
    }
205
 
206
    return 1;
207
}
208
 
209
# Parse_Changes($changes_filename)
210
#
211
# Parses the changes file found at $changes_filename (which should be a
212
# fully qualified path and filename), and returns a hashref pointing to a
213
# Changes hash. Returns undef in the case of a failure (and sets $Error).
214
 
215
# Changes Hash format:
216
# {
217
#   'Architecture' => \@Architectures
218
#   'Binary' => \@Binaries
219
#   'Changed-By' => Changed-By
220
#   'Changes' => \@Changes lines
221
#   'Closes' => \@Bugs
222
#   'Description' => Description
223
#   'Files' => \@\%File Hashes
224
#   'Date' => RFC 822 timestamp
225
#   'Distribution' => \@Distributions
226
#   'Maintainer' => Maintainer
227
#   'Source' => Source
228
#   'Urgency' => Urgency
229
#   'Version' => Version
230
# }
231
 
232
# File Hash format:
233
# {
234
#   'Filename' => Filename (leaf node only)
235
#   'MD5Sum' => File MD5Sum
236
#   'Priority' => Requested archive priority
237
#   'Section' => Requested archive section
238
#   'Size' => File size (in bytes)
239
# }
240
 
241
sub Parse_Changes {
242
    use DebPool::GnuPG qw(:functions);
243
    use DebPool::Logging qw(:functions :facility :level);
244
 
245
    my($file) = @_;
246
    my(%result);
247
 
248
    # Read in the entire Changes file, stripping GPG encoding if we find
249
    # it. It should be small, this is fine.
250
 
251
    if (!open(CHANGES, '<', $file)) {
252
        $Error = "Couldn't open changes file '$file': $!";
253
        return undef;
254
    }
255
 
256
    my(@changes) = <CHANGES>;
257
    chomp(@changes);
258
    @changes = Strip_GPG(@changes);
259
    close(CHANGES);
260
 
261
    # Go through each of the primary fields, stuffing it into the result
262
    # hash if we find it.
263
 
264
    my($field);
265
    foreach $field (keys(%Changes_Fields)) {
266
        my(@lines) = grep(/^${field}:\s+/, @changes);
267
        if (-1 == $#lines) { # No match
268
            next;
269
        } elsif (0 < $#lines) { # Multiple matches
270
            Log_Message("Duplicate entries for field '$field'",
271
                        LOG_PARSE, LOG_WARNING);
272
        }
273
 
274
        $lines[0] =~ s/^${field}:\s+//;
275
 
276
        if ('string' eq $Changes_Fields{$field}) {
277
            $result{$field} = $lines[0];
278
        } elsif ('space_array' eq $Changes_Fields{$field}) {
279
            my(@array) = split(/\s+/, $lines[0]);
280
            $result{$field} = \@array;
281
        } elsif ('comma_array' eq $Changes_Fields{$field}) {
282
            my(@array) = split(/\s+,\s+/, $lines[0]);
283
            $result{$field} = \@array;
284
        }
285
    }
286
 
287
    # Now that we should have it, check to make sure we have a Format
288
    # header, and that it's format 1.7 (the only thing we grok).
289
 
290
    if (!defined($result{'Format'})) {
291
        Log_Message("No Format header found in changes file '$file'",
292
                    LOG_PARSE, LOG_ERROR);
293
        $Error = 'No Format header found';
294
        return undef;
295
    } elsif ('1.7' ne $result{'Format'}) {
296
        Log_Message("Unrecognized Format version '$result{'Format'}'",
297
                    LOG_PARSE, LOG_ERROR);
298
        $Error = 'Unrecognized Format version';
299
        return undef;
300
    }
301
 
302
    # Special case: Description. One-line entry, immediately after a line
303
    # with '^Description:'.
304
 
305
    my($count);
306
 
307
    for $count (0..$#changes) {
308
        if ($changes[$count] =~ m/^Description:/) {
309
            $result{'Description'} = $changes[$count+1];
310
        }
311
    }
312
 
313
    # Special case: Changes. Multi-line entry, starts one line after
314
    # '^Changes:', goes until we hit the Files header.
315
 
316
    my($found) = 0;
317
    my(@changelines);
318
 
319
    for $count (0..$#changes) {
320
        if ($found) {
321
            if ($changes[$count] =~ m/^Files:/) {
322
                $found = 0;
323
            } else {
324
                push(@changelines, $changes[$count]);
325
            }
326
        } else {
327
            if ($changes[$count] =~ m/^Changes:/) {
328
                $found = 1;
329
            }
330
        }
331
    }
332
 
333
    $result{'Changes'} = \@changelines;
334
 
335
    # The Files section is a special case. It starts on the line after the
336
    # 'Files:' header, and goes until we hit a blank line, or the end of
337
    # the data.
338
 
339
    my(@files);
340
 
341
    for $count (0..$#changes) {
342
        if ($found) {
343
            if ($changes[$count] =~ m/^\s*$/) { # Blank line
344
                $found = 0; # No longer in Files
345
            } elsif ($changes[$count] =~ m/\s*([[:xdigit:]]+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+)/) {
346
                my($md5, $size, $sec, $pri, $file) = ($1, $2, $3, $4, $5);
347
                push(@files, {
348
                    'Filename' => $file,
349
                    'MD5Sum' => $md5,
350
                    'Priority' => $pri,
351
                    'Section' => $sec,
352
                    'Size' => $size,
353
                });
354
            } else { # What's this doing here?
355
                my($msg) = 'Unrecognized data in Files section of changes file';
356
                $msg .= " '$file'";
357
                Log_Message($msg, LOG_PARSE, LOG_WARNING);
358
            }
359
        } else {
360
            if ($changes[$count] =~ m/^Files:/) {
361
                $found = 1;
362
            }
363
        }
364
    }
365
 
366
    $result{'Files'} = \@files;
367
 
368
    return \%result;
369
}
370
 
371
# Parse_DSC($dsc_filename)
372
#
373
# Parses the dsc file found at $dsc_filename (which should be a fully
374
# qualified path and filename), and returns a hashref pointing to a DSC
375
# hash. Returns undef in the case of a failure (and sets $Error).
376
 
377
# DSC Hash format:
378
# {
379
#   'Format' => Format
380
#   'Source' => Source
381
#   'Binary' => \@Binaries
382
#   'Maintainer' => Maintainer
383
#   'Architecture' => \@Architectures
384
#   'Standards-Version' => Standards-Version
385
#   'Build-Depends' => Build-Depends
386
#   'Build-Depends-Indep' => Build-Depends-Indep
387
#   'Files' => \@\%Filehash
388
# }
389
 
390
# File Hash format:
391
# {
392
#   'Filename' => Filename (leaf node only)
393
#   'MD5Sum' => File MD5Sum
394
#   'Size' => File size (in bytes)
395
# }
396
 
397
sub Parse_DSC {
398
    use DebPool::GnuPG qw(:functions);
399
    use DebPool::Logging qw(:functions :facility :level);
400
 
401
    my($file) = @_;
402
    my(%result);
403
 
404
    # Read in the entire DSC file, stripping GPG encoding if we find it. It
405
    # should be small, this is fine.
406
 
407
    if (!open(DSC, '<', $file)) {
408
        $Error = "Couldn't open dsc file '$file': $!";
409
        return undef;
410
    }
411
 
412
    my(@dsc) = <DSC>;
413
    chomp(@dsc);
414
    @dsc = Strip_GPG(@dsc);
415
    close(DSC);
416
 
417
    # Go through each of the primary fields, stuffing it into the result
418
    # hash if we find it.
419
 
420
    my($field);
421
    foreach $field (keys(%DSC_Fields)) {
422
        my(@lines) = grep(/^${field}:\s+/, @dsc);
423
        if (-1 == $#lines) { # No match
424
            next;
425
        } elsif (0 < $#lines) { # Multiple matches
426
            Log_Message("Duplicate entries for field '$field'",
427
                        LOG_PARSE, LOG_WARNING);
428
        }
429
 
430
        $lines[0] =~ s/^${field}:\s+//;
431
 
432
        if ('string' eq $DSC_Fields{$field}) {
433
            $result{$field} = $lines[0];
434
        } elsif ('space_array' eq $DSC_Fields{$field}) {
435
            my(@array) = split(/\s+/, $lines[0]);
436
            $result{$field} = \@array;
437
        } elsif ('comma_array' eq $DSC_Fields{$field}) {
438
            my(@array) = split(/\s+,\s+/, $lines[0]);
439
            $result{$field} = \@array;
440
        }
441
    }
442
 
443
    # Now that we should have it, check to make sure we have a Format
444
    # header, and that it's format 1.0 (the only thing we grok).
445
 
446
    if (!defined($result{'Format'})) {
447
        Log_Message("No Format header found in dsc file '$file'",
448
                    LOG_PARSE, LOG_ERROR);
449
        $Error = 'No Format header found';
450
        return undef;
451
    } elsif ('1.0' ne $result{'Format'}) {
452
        Log_Message("Unrecognized Format version '$result{'Format'}'",
453
                    LOG_PARSE, LOG_ERROR);
454
        $Error = 'Unrecognized Format version';
455
        return undef;
456
    }
457
 
458
    # The Files section is a special case. It starts on the line after the
459
    # 'Files:' header, and goes until we hit a blank line, or the end of
460
    # the data.
461
 
462
    # In fact, it's even more special than that; it includes, first, an entry
463
    # for the DSC file itself...
464
 
465
    my($count);
466
    my($found) = 0;
467
    my(@files);
468
 
469
    my(@temp) = split(/\//, $file);
470
    my($dsc_leaf) = pop(@temp);
471
 
472
    my($cmd_result) = `/usr/bin/md5sum $file`;
473
    $cmd_result =~ m/^([[:xdigit:]]+)\s+/;
474
    my($dsc_md5) = $1;
475
 
476
    my(@stat) = stat($file);
477
    if (!@stat) {
478
        $Error = "Couldn't stat DSC file '$file'";
479
        return undef;
480
    }
481
    my($dsc_size) = $stat[7];
482
 
483
    push(@files, {
484
        'Filename' => $dsc_leaf,
485
        'MD5Sum' => $dsc_md5,
486
        'Size' => $dsc_size,
487
    });
488
 
489
    for $count (0..$#dsc) {
490
        if ($found) {
491
            if ($dsc[$count] =~ m/^\s*$/) { # Blank line
492
                $found = 0; # No longer in Files
493
            } elsif ($dsc[$count] =~ m/\s*([[:xdigit:]]+)\s+(\d+)\s+(\S+)/) {
494
                my($md5, $size, $file) = ($1, $2, $3);
495
                push(@files, {
496
                    'Filename' => $file,
497
                    'MD5Sum' => $md5,
498
                    'Size' => $size,
499
                });
500
            } else { # What's this doing here?
501
                my($msg) = 'Unrecognized data in Files section of dsc file';
502
                $msg .= " '$file'";
503
                Log_Message($msg, LOG_PARSE, LOG_WARNING);
504
            }
505
        } else {
506
            if ($dsc[$count] =~ m/^Files:/) {
507
                $found = 1;
508
            }
509
        }
510
    }
511
 
512
    $result{'Files'} = \@files;
513
 
514
    return \%result;
515
}
516
 
517
# Generate_List($distribution, $section, $arch)
518
#
519
# Generates a Packages (or Sources) file for the given distribution,
520
# section, and architecture (with 'source' being a special value for
521
# Sources). Returns the filename of the generated file on success, or undef
522
# (and sets $Error) on failure. Note that requests for an 'all' list are
523
# ignored - however, every non-source arch gets 'all' files.
524
 
525
sub Generate_List {
526
    use DebPool::Config qw(:vars);
527
    use DebPool::DB qw(:functions :vars);
528
    use DebPool::Dirs qw(:functions);
529
 
530
    my($distribution, $section, $arch) = @_;
531
 
532
    my(%packages);
533
 
534
    if ('all' eq $arch) {
535
        $Error = "No point in generating Packages file for binary-all";
536
        return undef;
537
    }
538
 
539
    my(@sources) = grep($ComponentDB{$distribution}->{$_} eq $section,
540
                        keys(%{$ComponentDB{$distribution}}));
541
 
542
    my($tmpfile_handle, $tmpfile_name) = tempfile();
543
 
544
    my($source);
545
 
546
    # Dump the data from pool/*/*/pkg_ver.{package,source} into the list.
547
 
548
    # FIXME: This needs to be refactored. Needs it pretty badly, in fact.
549
 
550
    if ('source' eq $arch) {
551
        foreach $source (@sources) {
552
            my($pool) = join('/',
553
                ($Options{'pool_dir'}, PoolDir($source, $section), $source));
554
            my($version) = Get_Version($distribution, $source, 'meta');
555
            my($target) = "$pool/${source}_" . Strip_Epoch($version);
556
            $target .= '.source';
557
 
558
            # Source files aren't always present.
559
            next if (!open(SRC, '<', "$target"));
560
 
561
            print $tmpfile_handle <SRC>;
562
            close(SRC);
563
        }
564
    } else {
565
        foreach $source (@sources) {
566
            my($pool) = join('/',
567
                ($Options{'pool_dir'}, PoolDir($source, $section), $source));
568
            my($version) = Get_Version($distribution, $source, 'meta');
569
            my($target) = "$pool/${source}_" . Strip_Epoch($version);
570
            $target .= '.package';
571
 
572
            if (!open(PKG, '<', "$target")) {
573
                my($msg) = "Skipping package entry for all packages from ";
574
                $msg .= "${source}: couldn't open '$target' for reading: $!";
575
 
576
                Log_Message($msg, LOG_GENERAL, LOG_ERROR);
577
                next;
578
            }
579
 
580
            # Playing around with the record separator ($/) to make this
581
            # easier.
582
 
583
            my($backup_RS) = $/;
584
            $/ = "";
585
 
586
            my(@entries) = <PKG>;
587
            close(PKG);
588
 
589
            $/ = $backup_RS;
590
 
591
            # Pare it down to the relevant entries, and print those out.
592
 
593
            @entries = grep(/\nArchitecture: ($arch|all)\n/, @entries);
594
            print $tmpfile_handle @entries;
595
        }
596
    }
597
 
598
    close($tmpfile_handle);
599
 
600
    return $tmpfile_name;
601
}
602
 
603
# Install_Package($changes, $Changes_hashref, $DSC, $DSC_hashref, \@distributions)
604
#
605
# Install all of the package files for $Changes_hashref (which should
606
# be a Parse_Changes result hash) into the pool directory, and install
607
# the file in $changes to the installed directory. Also generates (and
608
# installes) .package and .source meta-data files. It also updates the
609
# Version database for the listed distributions. Returns 1 if successful, 0
610
# if not (and sets $Error).
611
 
612
sub Install_Package {
613
    use DebPool::Config qw(:vars);
614
    use DebPool::Dirs qw(:functions);
615
    use DebPool::DB qw(:functions :vars);
616
    use DebPool::Util qw(:functions);
617
 
618
    my($changes, $chg_hashref, $dsc, $dsc_hashref, $distributions) = @_;
619
 
620
    my($incoming_dir) = $Options{'incoming_dir'};
621
    my($installed_dir) = $Options{'installed_dir'};
622
    my($pool_dir) = $Options{'pool_dir'};
623
 
624
    my($pkg_name) = $chg_hashref->{'Source'};
625
    my($pkg_ver) = $chg_hashref->{'Version'};
626
 
627
    my($guess_section) = Guess_Section($chg_hashref);
628
    my($pkg_dir) = join('/',
629
        ($pool_dir, PoolDir($pkg_name, $guess_section), $pkg_name));
630
 
631
    # Make sure the package directory exists (and is a directory!)
632
 
9 magnus 633
    Tree_Mkdir($pkg_dir, $Options{'pool_dir_mode'}) or return 0;
1 magnus 634
 
635
    # Walk the File Hash, trying to install each listed file into the
636
    # pool directory.
637
 
638
    my($filehash);
639
 
640
    foreach $filehash (@{$chg_hashref->{'Files'}}) {
641
        my($file) = $filehash->{'Filename'};
642
        if (!Move_File("${incoming_dir}/${file}", "${pkg_dir}/${file}",
643
                $Options{'pool_file_mode'})) {
644
            $Error = "Failed to move '${incoming_dir}/${file}' ";
645
            $Error .= "to '${pkg_dir}/${file}': ${DebPool::Util::Error}";
646
            return 0;
647
        }
648
    }
649
 
650
    # Generate and install .package and .source metadata files.
651
 
652
    my($pkg_file) = Generate_Package($chg_hashref);
653
 
654
    if (!defined($pkg_file)) {
655
        $Error = "Failed to generate .package file: $Error";
656
        return undef;
657
    }
658
 
659
    my($target) = "$pkg_dir/${pkg_name}_" . Strip_Epoch($pkg_ver) . '.package';
660
 
661
    if (!Move_File($pkg_file, $target, $Options{'pool_file_mode'})) {
662
        $Error = "Failed to move '$pkg_file' to '$target': ";
663
        $Error .= $DebPool::Util::Error;
664
        return 0;
665
    }
666
 
667
    if (defined($dsc) && defined($dsc_hashref)) {
668
        my($src_file) = Generate_Source($dsc, $dsc_hashref, $chg_hashref);
669
 
670
        if (!defined($src_file)) {
671
            $Error = "Failed to generate .source file: $Error";
672
            return undef;
673
        }
674
 
675
        $target = "$pkg_dir/${pkg_name}_" . Strip_Epoch($pkg_ver) . '.source';
676
 
677
        if (!Move_File($src_file, $target, $Options{'pool_file_mode'})) {
678
            $Error = "Failed to move '$src_file' to '$target': ";
679
            $Error .= $DebPool::Util::Error;
680
            return 0;
681
        }
682
    }
683
 
684
    # Finally, try to install the changes file to the installed directory.
685
 
686
    if (!Move_File("$incoming_dir/$changes", "$installed_dir/$changes",
687
            $Options{'installed_file_mode'})) {
688
        $Error = "Failed to move '$incoming_dir/$changes' to ";
689
        $Error .= "'$installed_dir/$changes': ${DebPool::Util::Error}";
690
        return 0;
691
    }
692
 
693
    # Update the various databases.
694
 
695
    my($distribution);
696
 
697
    # This whole block is just to calculate the component. What a stupid
698
    # setup - it should be in the changes file. Oh well.
699
 
700
    my(@filearray) = @{$chg_hashref->{'Files'}};
701
    my($fileref) = $filearray[0];
702
    my($section) = $fileref->{'Section'};
703
    my($component) = Strip_Subsection($section);
704
 
705
    foreach $distribution (@{$distributions}) {
706
        Set_Versions($distribution, $pkg_name, $pkg_ver,
707
            $chg_hashref->{'Files'});
708
        $ComponentDB{$distribution}->{$pkg_name} = $component;
709
    }
710
 
711
    return 1;
712
}
713
 
714
# Reject_Package($changes, $chg_hashref)
715
#
716
# Move all of the package files for $chg_hashref (which should be a
717
# Parse_Changes result hash) into the rejected directory, as well as the
718
# file in $changes. Returns 1 if successful, 0 if not (and sets $Error).
719
 
720
sub Reject_Package {
721
    use DebPool::Config qw(:vars);
722
    use DebPool::DB qw(:functions);
723
    use DebPool::Util qw(:functions);
724
 
725
    my($changes, $chg_hashref) = @_;
726
 
727
    my($incoming_dir) = $Options{'incoming_dir'};
728
    my($reject_dir) = $Options{'reject_dir'};
729
    my($reject_file_mode) = $Options{'reject_file_mode'};
730
 
731
    # Walk the File Hash, moving each file to the rejected directory.
732
 
733
    my($filehash);
734
 
735
    foreach $filehash (@{$chg_hashref->{'Files'}}) {
736
        my($file) = $filehash->{'Filename'};
737
        if (!Move_File("$incoming_dir/$file", "$reject_dir/$file",
738
                $reject_file_mode)) {
739
            $Error = "Failed to move '$incoming_dir/$file' ";
740
            $Error .= "to '$reject_dir/$file': ${DebPool::Util::Error}";
741
            return 0;
742
        }
743
    }
744
 
745
    # Now move the changes file to the rejected directory, as well.
746
 
747
    if (!Move_File("$incoming_dir/$changes", "$reject_dir/$changes",
748
            $reject_file_mode)) {
749
        $Error = "Failed to move '$incoming_dir/$changes' to ";
750
        $Error .= "'$reject_dir/$changes': ${DebPool::Util::Error}";
751
        return 0;
752
    }
753
 
754
    return 1;
755
}
756
 
757
# Verify_MD5($file, $md5)
758
#
759
# Verifies the MD5 checksum of $file against $md5. Returns 1 if it matches,
760
# 0 if it doesn't, and undef (also setting $Error) if an error occurs. This
761
# routine uses the dpkg md5sum utility, to avoid pulling in a dependancy on
762
# Digest::MD5.
763
 
764
sub Verify_MD5 {
765
    use DebPool::Logging qw(:functions :facility :level);
10 magnus 766
    use Digest::MD5;
1 magnus 767
 
768
    my($file, $md5) = @_;
10 magnus 769
    my($fh);
1 magnus 770
 
771
    # Read in and mangle the md5 output.
772
 
10 magnus 773
    unless (open($fh, '<', $file) && binmode($fh)) {
774
        my($msg) = "Can't open '$file' for reading: $!";
1 magnus 775
        Log_Message($msg, LOG_GENERAL, LOG_ERROR);
776
        return 0;
777
    }
778
 
10 magnus 779
    my($digester) = new Digest::MD5;
780
    my($check_md5);
781
    eval { # addfile can croak
782
        $check_md5 = $digester->addfile($fh)->hexdigest;
783
    };
784
    if ($@) {
785
        Log_Message("Failed to compute MD5 checksum for '$file': $@",
786
                    LOG_GENERAL, LOG_ERROR);
1 magnus 787
        return 0;
788
    }
789
 
790
    if ($md5 ne $check_md5) {
791
        my($msg) = "MD5 checksum failure: file '$file', ";
792
        $msg .= "expected '$md5', got '$check_md5'";
793
        Log_Message($msg, LOG_GENERAL, LOG_ERROR);
794
        return 0;
795
    }
796
 
797
    return 1;
798
}
799
 
800
# Audit_Package($package, $chg_hashref)
801
#
802
# Delete a package and changes files for the named (source) package which
803
# are not referenced by any version currently found in the various release
804
# databases. Returns the number of files unlinked (which may be 0), or
805
# undef (and sets $Error) on an error.
806
 
807
sub Audit_Package {
808
    use DebPool::Config qw(:vars);
809
    use DebPool::Dirs qw(:functions);
810
    use DebPool::Logging qw(:functions :facility :level);
811
 
812
    my($package, $changes_hashref) = @_;
813
 
814
    my($installed_dir) = $Options{'installed_dir'};
815
    my($pool_dir) = $Options{'pool_dir'};
816
 
817
    my($section) = Guess_Section($changes_hashref);
818
    my($package_dir) = join('/',
819
        ($pool_dir, PoolDir($package, $section), $package));
820
 
821
    my(@changes) = grep(/${package}_/, Scan_Changes($installed_dir));
822
 
823
    my($pool_scan) = Scan_All($package_dir);
824
    if (!defined($pool_scan)) {
825
        $Error = $DebPool::Dirs::Error;
826
        return undef;
827
    }
828
    my(@pool_files) = @{$pool_scan};
829
 
830
    # Go through each file found in the pool directory, and determine its
831
    # version. If it isn't in the current version tables, unlink it.
832
 
833
    my($file);
834
    my($unlinked) = 0;
835
    foreach $file (@pool_files) {
836
        my($orig) = 0;
837
        my($deb) = 0;
838
        my($src) = 0;
839
        my($bin_package, $version);
840
 
841
        if ($file =~ m/^([^_]+)_([^_]+)\.orig\.tar\.gz$/) { # orig.tar.gz
842
            $bin_package = $1;
843
            $version = $2;
844
            $src = 1;
845
            $orig = 1;
846
        } elsif ($file =~ m/^([^_]+)_([^_]+)\.tar\.gz$/) { # tar.gz
847
            $bin_package = $1;
848
            $version = $2;
849
            $src = 1;
850
        } elsif ($file =~ m/^([^_]+)_([^_]+)\.diff\.gz$/) { # diff.gz
851
            $bin_package = $1;
852
            $version = $2;
853
            $src = 1;
854
        } elsif ($file =~ m/^([^_]+)_([^_]+)\.dsc$/) { # dsc
855
            $bin_package = $1;
856
            $version = $2;
857
            $src = 1;
858
        } elsif ($file =~ m/^([^_]+)_([^_]+)_.+\.deb$/) { # deb
859
            $bin_package = $1;
860
            $version = $2;
861
            $deb = 1;
862
        } elsif ($file =~ m/^([^_]+)_([^_]+)_.+\.udeb$/) { # udeb
863
            $bin_package = $1;
864
            $version = $2;
865
            $deb = 1;
866
        } elsif ($file =~ m/^([^_]+)_([^_]+)\.package$/) { # package metadata
867
            $bin_package = $1;
868
            $version = $2;
869
        } elsif ($file =~ m/^([^_]+)_([^_]+)\.source$/) { # source metadata
870
            $bin_package = $1;
871
            $version = $2;
872
        } else {
873
            Log_Message("Couldn't figure out filetype for '$package_dir/$file'",
874
                LOG_AUDIT, LOG_ERROR);
875
            next;
876
        }
877
 
878
        # Skip it if we recognize it as a valid version.
879
 
880
        my($matched) = 0;
881
        my($dist);
882
        foreach $dist (@{$Options{'realdists'}}) {
883
            my($ver_pkg);
884
            if ($src) {
885
                $ver_pkg = 'source';
886
            } elsif ($deb) {
887
                $ver_pkg = $bin_package;
888
            } else {
889
                $ver_pkg = 'meta';
890
            }
891
 
892
            my($dist_ver) = Get_Version($dist, $package, $ver_pkg);
893
            next if (!defined($dist_ver)); # No version in specified dist
894
            $dist_ver = Strip_Epoch($dist_ver);
895
            if ($orig) { $dist_ver =~ s/-.+$//; }
896
            if ($version eq $dist_ver) { $matched = 1; }
897
        }
898
        next if $matched;
899
 
900
        # Otherwise, unlink it.
901
 
902
        if (unlink("$package_dir/$file")) {
903
            $unlinked += 1;
904
            Log_Message("Unlinked obsolete pool file '$package_dir/$file'",
905
                LOG_AUDIT, LOG_DEBUG);
906
        } else {
907
            Log_Message("Couldn't obsolete pool file '$package_dir/$file'",
908
                LOG_AUDIT, LOG_ERROR);
909
        }
910
    }
911
 
912
    foreach $file (@changes) {
913
        $file =~ m/^[^_]+_([^_]+)_.+$/; # changes
914
        my($version) = $1;
915
 
916
        my($matched) = 0;
917
        my($dist);
918
        foreach $dist (@{$Options{'realdists'}}) {
919
            my($dist_ver) = Get_Version($dist, $package, 'meta');
920
            next if (!defined($dist_ver)); # No version in specified dist
921
            $dist_ver = Strip_Epoch($dist_ver);
922
            if ($version eq $dist_ver) { $matched = 1; }
923
        }
924
        next if $matched;
925
 
926
        if (unlink("$installed_dir/$file")) {
927
            $unlinked += 1;
928
            Log_Message("Unlinked obsolete changes file " .
929
                "'$installed_dir/$file'", LOG_AUDIT, LOG_DEBUG);
930
        } else {
931
            Log_Message("Couldn't obsolete changes file " .
932
                "'$installed_dir/$file'", LOG_AUDIT, LOG_ERROR);
933
        }
934
    }
935
 
936
    return $unlinked;
937
}
938
 
939
# Generate_Package($chg_hashref)
940
#
941
# Generates a .package metadata file (Packages entries for each binary
942
# package) in the tempfile area, and returns the filename. Returns undef
943
# (and sets $Error) on failure.
944
 
945
sub Generate_Package {
946
    use DebPool::Config qw(:vars);
947
    use DebPool::Dirs qw(:functions);
948
    use DebPool::Logging qw(:functions :facility :level);
949
 
950
    my($changes_data) = @_;
951
    my($source) = $changes_data->{'Source'};
952
    my(@files) = @{$changes_data->{'Files'}};
953
    my($pool_base) = PoolBasePath();
954
 
955
    # Grab a temporary file.
956
 
957
    my($tmpfile_handle, $tmpfile_name) = tempfile();
958
 
959
    my(@packages) = @{$changes_data->{'Binary'}};
960
    my(@architectures) = @{$changes_data->{'Architecture'}};
961
    @architectures = grep(!/source/, @architectures); # Source is on it's own.
962
 
963
    my($package, $arch);
964
 
965
    foreach $package (@packages) {
966
        foreach $arch (@architectures) {
967
            # Construct a pattern to match the filename and nothing else.
968
            # This used to be an exact match using the source version, but
969
            # Debian's standards are sort of insane, and the version number
970
            # on binary files is not always the same as that on the source
971
            # file (nor is it even something simple like "source version
972
            # without the epoch" -- it is more or less arbitrary, as long
973
            # as it is a well-formed version number).
974
 
975
            my($filepat) = "${package}_.*_${arch}\\.deb";
976
            $filepat =~ s/\+/\\\+/;
977
 
978
            my($section) = Guess_Section($changes_data);
979
            my($pool) = join('/', (PoolDir($source, $section), $source));
980
 
981
            my($marker) = -1;
982
            my($count) = 0;
983
 
984
            # Step through each file, match against filename. Save matches
985
            # for later use.
986
 
987
            for $count (0..$#files) {
988
                if ($files[$count]->{'Filename'} =~ m/$filepat/) {
989
                    $marker = $count;
990
                }
991
            }
992
 
993
            # The changes file has a stupid quirk; it puts all binaries from
994
            # a package in the Binary: line, even if they weren't built (for
995
            # example, an Arch: all doc package when doing an arch-only build
996
            # for a port). So if we didn't find a .deb file for it, assume
997
            # that it's one of those, and skip, rather than choking on it.
998
 
999
            next if (-1 == $marker);
1000
 
1001
            # Run Dpkg_Info to grab the dpkg --info data on the package.
1002
 
1003
            my($file) = $files[$marker]->{'Filename'};
1004
            my($info) = Dpkg_Info("$Options{'pool_dir'}/$pool/$file");
1005
 
1006
            # Dump all of our data into the metadata tempfile.
1007
 
1008
            print $tmpfile_handle "Package: $package\n";
1009
 
1010
            if (defined($info->{'Priority'})) {
1011
                print $tmpfile_handle "Priority: $info->{'Priority'}\n";
1012
            }
1013
 
1014
            if (defined($info->{'Section'})) {
1015
                print $tmpfile_handle "Section: $info->{'Section'}\n";
1016
            }
1017
 
1018
            if (defined($info->{'Essential'})) {
1019
                print $tmpfile_handle "Essential: $info->{'Essential'}\n";
1020
            }
1021
 
1022
            print $tmpfile_handle "Installed-Size: $info->{'Installed-Size'}\n";
1023
 
1024
            print $tmpfile_handle "Maintainer: $changes_data->{'Maintainer'}\n";
1025
            print $tmpfile_handle "Architecture: $arch\n";
1026
            print $tmpfile_handle "Source: $source\n";
1027
            print $tmpfile_handle "Version: $changes_data->{'Version'}\n";
1028
 
1029
            # All of the inter-package relationships go together, and any
1030
            # one of them can potentially be empty (and omitted).
1031
 
1032
            my($field);
1033
            foreach $field (@Relationship_Fields) {
1034
                if (defined($info->{$field})) {
1035
                    print $tmpfile_handle "${field}: $info->{$field}\n";
1036
                }
1037
            }
1038
 
1039
            # And now, some stuff we can grab out of the parsed changes
1040
            # data far more easily than anywhere else.
1041
 
1042
            print $tmpfile_handle "Filename: $pool_base/$pool/$file\n";
1043
 
1044
            print $tmpfile_handle "Size: $files[$marker]->{'Size'}\n";
1045
            print $tmpfile_handle "MD5sum: $files[$marker]->{'MD5Sum'}\n";
1046
 
1047
            print $tmpfile_handle "Description: $info->{'Description'}";
1048
        }
1049
 
1050
        print $tmpfile_handle "\n";
1051
    }
1052
 
1053
    # All done
1054
 
1055
    close($tmpfile_handle);
1056
    return $tmpfile_name;
1057
}
1058
 
1059
# Generate_Source($dsc, $dsc_hashref, $changes_hashref)
1060
#
1061
# Generates a .source metadata file (Sources entries for the source
1062
# package) in the tempfile area, and returns the filename. Returns undef
1063
# (and sets $Error) on failure.
1064
 
1065
sub Generate_Source {
1066
    use DebPool::Dirs qw(:functions);
1067
    use DebPool::Logging qw(:functions :facility :level);
1068
 
1069
    my($dsc, $dsc_data, $changes_data) = @_;
1070
    my($source) = $dsc_data->{'Source'};
1071
    my(@files) = @{$dsc_data->{'Files'}};
1072
 
1073
    # Figure out the priority and section, using the DSC filename and
1074
    # the Changes file data.
1075
 
1076
    my($section, $priority);
1077
    my($filehr);
1078
    foreach $filehr (@{$changes_data->{'Files'}}) {
1079
        if ($filehr->{'Filename'} eq $dsc) {
1080
            $section = $filehr->{'Section'};
1081
            $priority = $filehr->{'Priority'};
1082
        }
1083
    }
1084
 
1085
    # Grab a temporary file.
1086
 
1087
    my($tmpfile_handle, $tmpfile_name) = tempfile();
1088
 
1089
    # Dump out various metadata.
1090
 
1091
    print $tmpfile_handle "Package: $source\n";
1092
    print $tmpfile_handle "Binary: " . join(', ', @{$dsc_data->{'Binary'}}) . "\n";
1093
    print $tmpfile_handle "Version: $dsc_data->{'Version'}\n";
1094
    print $tmpfile_handle "Priority: $priority\n";
1095
    print $tmpfile_handle "Section: $section\n";
1096
    print $tmpfile_handle "Maintainer: $dsc_data->{'Maintainer'}\n";
1097
 
1098
    if (defined($dsc_data->{'Build-Depends'})) {
1099
        print $tmpfile_handle 'Build-Depends: ';
1100
        print $tmpfile_handle join(', ', @{$dsc_data->{'Build-Depends'}}) . "\n";
1101
    }
1102
 
1103
    if (defined($dsc_data->{'Build-Depends-Indep'})) {
1104
        print $tmpfile_handle 'Build-Depends-Indep: ';
1105
        print $tmpfile_handle join(', ', @{$dsc_data->{'Build-Depends-Indep'}}) . "\n";
1106
    }
1107
 
1108
    print $tmpfile_handle 'Architecture: ';
1109
    print $tmpfile_handle join(' ', @{$dsc_data->{'Architecture'}}) . "\n";
1110
 
1111
    print $tmpfile_handle "Standards-Version: $dsc_data->{'Standards-Version'}\n";
1112
    print $tmpfile_handle "Format: $dsc_data->{'Format'}\n";
1113
    print $tmpfile_handle "Directory: " .  join('/',
1114
        (PoolBasePath(), PoolDir($source, $section), $source)) . "\n";
1115
 
1116
    print $tmpfile_handle "Files:\n";
1117
 
1118
    my($fileref);
1119
    foreach $fileref (@files) {
1120
        print $tmpfile_handle " $fileref->{'MD5Sum'}";
1121
        print $tmpfile_handle " $fileref->{'Size'}";
1122
        print $tmpfile_handle " $fileref->{'Filename'}\n";
1123
    }
1124
 
1125
    print $tmpfile_handle "\n";
1126
 
1127
    # All done
1128
 
1129
    close($tmpfile_handle);
1130
    return $tmpfile_name;
1131
}
1132
 
1133
# Dpkg_Info($file)
1134
#
1135
# Runs dpkg --info on $file, and returns a hash of relevant information.
1136
#
1137
# Internal support function for Generate_Package.
1138
 
1139
sub Dpkg_Info {
1140
    my($file) = @_;
1141
    my(%result);
1142
 
1143
    # Grab the info from dpkg --info.
1144
 
1145
    my(@info) = `/usr/bin/dpkg --info $file`;
1146
    my($smashed) = join('', @info);
1147
 
1148
    # Look for each of these fields in the info. All are single line values,
1149
    # so the matching is fairly easy.
1150
 
1151
    my($field);
1152
 
1153
    foreach $field (@Info_Fields, @Relationship_Fields) {
1154
        if ($smashed =~ m/\n ${field}:\s+(\S.*)\n/) {
1155
            $result{$field} = $1;
1156
        }
1157
    }
1158
 
1159
    # And, finally, grab the description.
1160
 
1161
    my($line);
1162
    my($found) = 0;
1163
    foreach $line (@info) {
1164
        if ($found) {
1165
            $line =~ s/^ //;
1166
            $result{'Description'} .= $line;
1167
        } elsif ($line =~ m/^ Description: (.+)/) {
1168
            $result{'Description'} = "$1\n";
1169
            $found = 1;
1170
        }
1171
    }
1172
 
1173
    return \%result;
1174
}
1175
 
1176
# Install_List($archive, $component, $architecture, $listfile, $gzfile)
1177
#
1178
# Installs a distribution list file (from Generate_List), along with an
1179
# optional gzipped version of the same file (if $gzfile is defined).
1180
# Returns 1 on success, or 0 (and sets $Error) on failure.
1181
 
1182
sub Install_List {
1183
    use DebPool::Config qw(:vars);
1184
    use DebPool::Dirs qw(:functions);
1185
 
1186
    my($archive, $component, $architecture, $listfile, $gzfile) = @_;
1187
 
1188
    my($dists_file_mode) = $Options{'dists_file_mode'};
1189
    my($inst_file) = "$Options{'dists_dir'}/";
1190
    $inst_file .= Archfile($archive, $component, $architecture, 0);
1191
 
1192
    # Now install the file(s) into the appropriate place(s).
1193
 
1194
    if (!Move_File($listfile, $inst_file, $dists_file_mode)) {
1195
        $Error = "Couldn't install distribution file '$listfile' ";
1196
        $Error .= "to '${inst_file}': ${DebPool::Util::Error}";
1197
        return 0;
1198
    }
1199
 
1200
    if (defined($gzfile) && !Move_File($gzfile, "${inst_file}.gz",
1201
            $dists_file_mode)) {
1202
        $Error = "Couldn't install gzipped distribution file '$gzfile' ";
1203
        $Error .= "to '${inst_file}.gz': ${DebPool::Util::Error}";
1204
        return 0;
1205
    }
1206
 
1207
    return 1;
1208
}
1209
 
1210
# Guess_Section($changes_hashref)
1211
#
1212
# Attempt to guess the freeness section of a package based on the data
1213
# for the first file listed in the changes.
1214
 
1215
sub Guess_Section {
1216
    # Pull out the primary section from the changes data. Note that this is
1217
    # a cheap hack, but it is mostly used when needing the pool directory
1218
    # section, which is based solely on freeness-sections (main, contrib,
1219
    # non-free).
1220
 
1221
    my($changes_hashref) = @_;
1222
 
1223
    my(@changes_files) = @{$changes_hashref->{'Files'}};
1224
    return $changes_files[0]->{'Section'};
1225
}
1226
 
1227
# Strip_Epoch($version)
1228
#
1229
# Strips any epoch data off of the version.
1230
 
1231
sub Strip_Epoch {
1232
    my($version) = @_;
1233
 
1234
    $version =~ s/^[^:]://;
1235
    return $version;
1236
}
1237
 
1238
END {}
1239
 
1240
1;
1241
 
1242
__END__
1243
 
1244
# vim:set tabstop=4 expandtab: