Subversion Repositories

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

Rev 5 | Rev 10 | 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);
766
 
767
    my($file, $md5) = @_;
768
 
769
    # Read in and mangle the md5 output.
770
 
771
    if (! -r $file) { # The file doesn't exist! Will be hard to checksum it...
772
        my($msg) = "MD5 checksum unavailable: file '$file' does not exist!";
773
        Log_Message($msg, LOG_GENERAL, LOG_ERROR);
774
        return 0;
775
    }
776
 
777
    my($cmd_result) = `/usr/bin/md5sum $file`;
778
    if (!$cmd_result) { # Failed to run md5sum for some reason
779
        my($msg) = "MD5 checksum unavailable: file '$file'";
780
        Log_Message($msg, LOG_GENERAL, LOG_ERROR);
781
        return 0;
782
    }
783
 
784
    $cmd_result =~ m/^([[:xdigit:]]+)\s+/;
785
    my($check_md5) = $1;
786
 
787
    if ($md5 ne $check_md5) {
788
        my($msg) = "MD5 checksum failure: file '$file', ";
789
        $msg .= "expected '$md5', got '$check_md5'";
790
        Log_Message($msg, LOG_GENERAL, LOG_ERROR);
791
        return 0;
792
    }
793
 
794
    return 1;
795
}
796
 
797
# Audit_Package($package, $chg_hashref)
798
#
799
# Delete a package and changes files for the named (source) package which
800
# are not referenced by any version currently found in the various release
801
# databases. Returns the number of files unlinked (which may be 0), or
802
# undef (and sets $Error) on an error.
803
 
804
sub Audit_Package {
805
    use DebPool::Config qw(:vars);
806
    use DebPool::Dirs qw(:functions);
807
    use DebPool::Logging qw(:functions :facility :level);
808
 
809
    my($package, $changes_hashref) = @_;
810
 
811
    my($installed_dir) = $Options{'installed_dir'};
812
    my($pool_dir) = $Options{'pool_dir'};
813
 
814
    my($section) = Guess_Section($changes_hashref);
815
    my($package_dir) = join('/',
816
        ($pool_dir, PoolDir($package, $section), $package));
817
 
818
    my(@changes) = grep(/${package}_/, Scan_Changes($installed_dir));
819
 
820
    my($pool_scan) = Scan_All($package_dir);
821
    if (!defined($pool_scan)) {
822
        $Error = $DebPool::Dirs::Error;
823
        return undef;
824
    }
825
    my(@pool_files) = @{$pool_scan};
826
 
827
    # Go through each file found in the pool directory, and determine its
828
    # version. If it isn't in the current version tables, unlink it.
829
 
830
    my($file);
831
    my($unlinked) = 0;
832
    foreach $file (@pool_files) {
833
        my($orig) = 0;
834
        my($deb) = 0;
835
        my($src) = 0;
836
        my($bin_package, $version);
837
 
838
        if ($file =~ m/^([^_]+)_([^_]+)\.orig\.tar\.gz$/) { # orig.tar.gz
839
            $bin_package = $1;
840
            $version = $2;
841
            $src = 1;
842
            $orig = 1;
843
        } elsif ($file =~ m/^([^_]+)_([^_]+)\.tar\.gz$/) { # tar.gz
844
            $bin_package = $1;
845
            $version = $2;
846
            $src = 1;
847
        } elsif ($file =~ m/^([^_]+)_([^_]+)\.diff\.gz$/) { # diff.gz
848
            $bin_package = $1;
849
            $version = $2;
850
            $src = 1;
851
        } elsif ($file =~ m/^([^_]+)_([^_]+)\.dsc$/) { # dsc
852
            $bin_package = $1;
853
            $version = $2;
854
            $src = 1;
855
        } elsif ($file =~ m/^([^_]+)_([^_]+)_.+\.deb$/) { # deb
856
            $bin_package = $1;
857
            $version = $2;
858
            $deb = 1;
859
        } elsif ($file =~ m/^([^_]+)_([^_]+)_.+\.udeb$/) { # udeb
860
            $bin_package = $1;
861
            $version = $2;
862
            $deb = 1;
863
        } elsif ($file =~ m/^([^_]+)_([^_]+)\.package$/) { # package metadata
864
            $bin_package = $1;
865
            $version = $2;
866
        } elsif ($file =~ m/^([^_]+)_([^_]+)\.source$/) { # source metadata
867
            $bin_package = $1;
868
            $version = $2;
869
        } else {
870
            Log_Message("Couldn't figure out filetype for '$package_dir/$file'",
871
                LOG_AUDIT, LOG_ERROR);
872
            next;
873
        }
874
 
875
        # Skip it if we recognize it as a valid version.
876
 
877
        my($matched) = 0;
878
        my($dist);
879
        foreach $dist (@{$Options{'realdists'}}) {
880
            my($ver_pkg);
881
            if ($src) {
882
                $ver_pkg = 'source';
883
            } elsif ($deb) {
884
                $ver_pkg = $bin_package;
885
            } else {
886
                $ver_pkg = 'meta';
887
            }
888
 
889
            my($dist_ver) = Get_Version($dist, $package, $ver_pkg);
890
            next if (!defined($dist_ver)); # No version in specified dist
891
            $dist_ver = Strip_Epoch($dist_ver);
892
            if ($orig) { $dist_ver =~ s/-.+$//; }
893
            if ($version eq $dist_ver) { $matched = 1; }
894
        }
895
        next if $matched;
896
 
897
        # Otherwise, unlink it.
898
 
899
        if (unlink("$package_dir/$file")) {
900
            $unlinked += 1;
901
            Log_Message("Unlinked obsolete pool file '$package_dir/$file'",
902
                LOG_AUDIT, LOG_DEBUG);
903
        } else {
904
            Log_Message("Couldn't obsolete pool file '$package_dir/$file'",
905
                LOG_AUDIT, LOG_ERROR);
906
        }
907
    }
908
 
909
    foreach $file (@changes) {
910
        $file =~ m/^[^_]+_([^_]+)_.+$/; # changes
911
        my($version) = $1;
912
 
913
        my($matched) = 0;
914
        my($dist);
915
        foreach $dist (@{$Options{'realdists'}}) {
916
            my($dist_ver) = Get_Version($dist, $package, 'meta');
917
            next if (!defined($dist_ver)); # No version in specified dist
918
            $dist_ver = Strip_Epoch($dist_ver);
919
            if ($version eq $dist_ver) { $matched = 1; }
920
        }
921
        next if $matched;
922
 
923
        if (unlink("$installed_dir/$file")) {
924
            $unlinked += 1;
925
            Log_Message("Unlinked obsolete changes file " .
926
                "'$installed_dir/$file'", LOG_AUDIT, LOG_DEBUG);
927
        } else {
928
            Log_Message("Couldn't obsolete changes file " .
929
                "'$installed_dir/$file'", LOG_AUDIT, LOG_ERROR);
930
        }
931
    }
932
 
933
    return $unlinked;
934
}
935
 
936
# Generate_Package($chg_hashref)
937
#
938
# Generates a .package metadata file (Packages entries for each binary
939
# package) in the tempfile area, and returns the filename. Returns undef
940
# (and sets $Error) on failure.
941
 
942
sub Generate_Package {
943
    use DebPool::Config qw(:vars);
944
    use DebPool::Dirs qw(:functions);
945
    use DebPool::Logging qw(:functions :facility :level);
946
 
947
    my($changes_data) = @_;
948
    my($source) = $changes_data->{'Source'};
949
    my(@files) = @{$changes_data->{'Files'}};
950
    my($pool_base) = PoolBasePath();
951
 
952
    # Grab a temporary file.
953
 
954
    my($tmpfile_handle, $tmpfile_name) = tempfile();
955
 
956
    my(@packages) = @{$changes_data->{'Binary'}};
957
    my(@architectures) = @{$changes_data->{'Architecture'}};
958
    @architectures = grep(!/source/, @architectures); # Source is on it's own.
959
 
960
    my($package, $arch);
961
 
962
    foreach $package (@packages) {
963
        foreach $arch (@architectures) {
964
            # Construct a pattern to match the filename and nothing else.
965
            # This used to be an exact match using the source version, but
966
            # Debian's standards are sort of insane, and the version number
967
            # on binary files is not always the same as that on the source
968
            # file (nor is it even something simple like "source version
969
            # without the epoch" -- it is more or less arbitrary, as long
970
            # as it is a well-formed version number).
971
 
972
            my($filepat) = "${package}_.*_${arch}\\.deb";
973
            $filepat =~ s/\+/\\\+/;
974
 
975
            my($section) = Guess_Section($changes_data);
976
            my($pool) = join('/', (PoolDir($source, $section), $source));
977
 
978
            my($marker) = -1;
979
            my($count) = 0;
980
 
981
            # Step through each file, match against filename. Save matches
982
            # for later use.
983
 
984
            for $count (0..$#files) {
985
                if ($files[$count]->{'Filename'} =~ m/$filepat/) {
986
                    $marker = $count;
987
                }
988
            }
989
 
990
            # The changes file has a stupid quirk; it puts all binaries from
991
            # a package in the Binary: line, even if they weren't built (for
992
            # example, an Arch: all doc package when doing an arch-only build
993
            # for a port). So if we didn't find a .deb file for it, assume
994
            # that it's one of those, and skip, rather than choking on it.
995
 
996
            next if (-1 == $marker);
997
 
998
            # Run Dpkg_Info to grab the dpkg --info data on the package.
999
 
1000
            my($file) = $files[$marker]->{'Filename'};
1001
            my($info) = Dpkg_Info("$Options{'pool_dir'}/$pool/$file");
1002
 
1003
            # Dump all of our data into the metadata tempfile.
1004
 
1005
            print $tmpfile_handle "Package: $package\n";
1006
 
1007
            if (defined($info->{'Priority'})) {
1008
                print $tmpfile_handle "Priority: $info->{'Priority'}\n";
1009
            }
1010
 
1011
            if (defined($info->{'Section'})) {
1012
                print $tmpfile_handle "Section: $info->{'Section'}\n";
1013
            }
1014
 
1015
            if (defined($info->{'Essential'})) {
1016
                print $tmpfile_handle "Essential: $info->{'Essential'}\n";
1017
            }
1018
 
1019
            print $tmpfile_handle "Installed-Size: $info->{'Installed-Size'}\n";
1020
 
1021
            print $tmpfile_handle "Maintainer: $changes_data->{'Maintainer'}\n";
1022
            print $tmpfile_handle "Architecture: $arch\n";
1023
            print $tmpfile_handle "Source: $source\n";
1024
            print $tmpfile_handle "Version: $changes_data->{'Version'}\n";
1025
 
1026
            # All of the inter-package relationships go together, and any
1027
            # one of them can potentially be empty (and omitted).
1028
 
1029
            my($field);
1030
            foreach $field (@Relationship_Fields) {
1031
                if (defined($info->{$field})) {
1032
                    print $tmpfile_handle "${field}: $info->{$field}\n";
1033
                }
1034
            }
1035
 
1036
            # And now, some stuff we can grab out of the parsed changes
1037
            # data far more easily than anywhere else.
1038
 
1039
            print $tmpfile_handle "Filename: $pool_base/$pool/$file\n";
1040
 
1041
            print $tmpfile_handle "Size: $files[$marker]->{'Size'}\n";
1042
            print $tmpfile_handle "MD5sum: $files[$marker]->{'MD5Sum'}\n";
1043
 
1044
            print $tmpfile_handle "Description: $info->{'Description'}";
1045
        }
1046
 
1047
        print $tmpfile_handle "\n";
1048
    }
1049
 
1050
    # All done
1051
 
1052
    close($tmpfile_handle);
1053
    return $tmpfile_name;
1054
}
1055
 
1056
# Generate_Source($dsc, $dsc_hashref, $changes_hashref)
1057
#
1058
# Generates a .source metadata file (Sources entries for the source
1059
# package) in the tempfile area, and returns the filename. Returns undef
1060
# (and sets $Error) on failure.
1061
 
1062
sub Generate_Source {
1063
    use DebPool::Dirs qw(:functions);
1064
    use DebPool::Logging qw(:functions :facility :level);
1065
 
1066
    my($dsc, $dsc_data, $changes_data) = @_;
1067
    my($source) = $dsc_data->{'Source'};
1068
    my(@files) = @{$dsc_data->{'Files'}};
1069
 
1070
    # Figure out the priority and section, using the DSC filename and
1071
    # the Changes file data.
1072
 
1073
    my($section, $priority);
1074
    my($filehr);
1075
    foreach $filehr (@{$changes_data->{'Files'}}) {
1076
        if ($filehr->{'Filename'} eq $dsc) {
1077
            $section = $filehr->{'Section'};
1078
            $priority = $filehr->{'Priority'};
1079
        }
1080
    }
1081
 
1082
    # Grab a temporary file.
1083
 
1084
    my($tmpfile_handle, $tmpfile_name) = tempfile();
1085
 
1086
    # Dump out various metadata.
1087
 
1088
    print $tmpfile_handle "Package: $source\n";
1089
    print $tmpfile_handle "Binary: " . join(', ', @{$dsc_data->{'Binary'}}) . "\n";
1090
    print $tmpfile_handle "Version: $dsc_data->{'Version'}\n";
1091
    print $tmpfile_handle "Priority: $priority\n";
1092
    print $tmpfile_handle "Section: $section\n";
1093
    print $tmpfile_handle "Maintainer: $dsc_data->{'Maintainer'}\n";
1094
 
1095
    if (defined($dsc_data->{'Build-Depends'})) {
1096
        print $tmpfile_handle 'Build-Depends: ';
1097
        print $tmpfile_handle join(', ', @{$dsc_data->{'Build-Depends'}}) . "\n";
1098
    }
1099
 
1100
    if (defined($dsc_data->{'Build-Depends-Indep'})) {
1101
        print $tmpfile_handle 'Build-Depends-Indep: ';
1102
        print $tmpfile_handle join(', ', @{$dsc_data->{'Build-Depends-Indep'}}) . "\n";
1103
    }
1104
 
1105
    print $tmpfile_handle 'Architecture: ';
1106
    print $tmpfile_handle join(' ', @{$dsc_data->{'Architecture'}}) . "\n";
1107
 
1108
    print $tmpfile_handle "Standards-Version: $dsc_data->{'Standards-Version'}\n";
1109
    print $tmpfile_handle "Format: $dsc_data->{'Format'}\n";
1110
    print $tmpfile_handle "Directory: " .  join('/',
1111
        (PoolBasePath(), PoolDir($source, $section), $source)) . "\n";
1112
 
1113
    print $tmpfile_handle "Files:\n";
1114
 
1115
    my($fileref);
1116
    foreach $fileref (@files) {
1117
        print $tmpfile_handle " $fileref->{'MD5Sum'}";
1118
        print $tmpfile_handle " $fileref->{'Size'}";
1119
        print $tmpfile_handle " $fileref->{'Filename'}\n";
1120
    }
1121
 
1122
    print $tmpfile_handle "\n";
1123
 
1124
    # All done
1125
 
1126
    close($tmpfile_handle);
1127
    return $tmpfile_name;
1128
}
1129
 
1130
# Dpkg_Info($file)
1131
#
1132
# Runs dpkg --info on $file, and returns a hash of relevant information.
1133
#
1134
# Internal support function for Generate_Package.
1135
 
1136
sub Dpkg_Info {
1137
    my($file) = @_;
1138
    my(%result);
1139
 
1140
    # Grab the info from dpkg --info.
1141
 
1142
    my(@info) = `/usr/bin/dpkg --info $file`;
1143
    my($smashed) = join('', @info);
1144
 
1145
    # Look for each of these fields in the info. All are single line values,
1146
    # so the matching is fairly easy.
1147
 
1148
    my($field);
1149
 
1150
    foreach $field (@Info_Fields, @Relationship_Fields) {
1151
        if ($smashed =~ m/\n ${field}:\s+(\S.*)\n/) {
1152
            $result{$field} = $1;
1153
        }
1154
    }
1155
 
1156
    # And, finally, grab the description.
1157
 
1158
    my($line);
1159
    my($found) = 0;
1160
    foreach $line (@info) {
1161
        if ($found) {
1162
            $line =~ s/^ //;
1163
            $result{'Description'} .= $line;
1164
        } elsif ($line =~ m/^ Description: (.+)/) {
1165
            $result{'Description'} = "$1\n";
1166
            $found = 1;
1167
        }
1168
    }
1169
 
1170
    return \%result;
1171
}
1172
 
1173
# Install_List($archive, $component, $architecture, $listfile, $gzfile)
1174
#
1175
# Installs a distribution list file (from Generate_List), along with an
1176
# optional gzipped version of the same file (if $gzfile is defined).
1177
# Returns 1 on success, or 0 (and sets $Error) on failure.
1178
 
1179
sub Install_List {
1180
    use DebPool::Config qw(:vars);
1181
    use DebPool::Dirs qw(:functions);
1182
 
1183
    my($archive, $component, $architecture, $listfile, $gzfile) = @_;
1184
 
1185
    my($dists_file_mode) = $Options{'dists_file_mode'};
1186
    my($inst_file) = "$Options{'dists_dir'}/";
1187
    $inst_file .= Archfile($archive, $component, $architecture, 0);
1188
 
1189
    # Now install the file(s) into the appropriate place(s).
1190
 
1191
    if (!Move_File($listfile, $inst_file, $dists_file_mode)) {
1192
        $Error = "Couldn't install distribution file '$listfile' ";
1193
        $Error .= "to '${inst_file}': ${DebPool::Util::Error}";
1194
        return 0;
1195
    }
1196
 
1197
    if (defined($gzfile) && !Move_File($gzfile, "${inst_file}.gz",
1198
            $dists_file_mode)) {
1199
        $Error = "Couldn't install gzipped distribution file '$gzfile' ";
1200
        $Error .= "to '${inst_file}.gz': ${DebPool::Util::Error}";
1201
        return 0;
1202
    }
1203
 
1204
    return 1;
1205
}
1206
 
1207
# Guess_Section($changes_hashref)
1208
#
1209
# Attempt to guess the freeness section of a package based on the data
1210
# for the first file listed in the changes.
1211
 
1212
sub Guess_Section {
1213
    # Pull out the primary section from the changes data. Note that this is
1214
    # a cheap hack, but it is mostly used when needing the pool directory
1215
    # section, which is based solely on freeness-sections (main, contrib,
1216
    # non-free).
1217
 
1218
    my($changes_hashref) = @_;
1219
 
1220
    my(@changes_files) = @{$changes_hashref->{'Files'}};
1221
    return $changes_files[0]->{'Section'};
1222
}
1223
 
1224
# Strip_Epoch($version)
1225
#
1226
# Strips any epoch data off of the version.
1227
 
1228
sub Strip_Epoch {
1229
    my($version) = @_;
1230
 
1231
    $version =~ s/^[^:]://;
1232
    return $version;
1233
}
1234
 
1235
END {}
1236
 
1237
1;
1238
 
1239
__END__
1240
 
1241
# vim:set tabstop=4 expandtab: