openpkg/lint-rpm.pl

Mon, 28 Jan 2013 17:37:18 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Mon, 28 Jan 2013 17:37:18 +0100
changeset 758
a2c6460cfb16
permissions
-rw-r--r--

Correct socket error reporting improvement with IPv6 portable code,
after helpful recommendation by Saúl Ibarra Corretgé on OSips devlist.

michael@428 1 ##
michael@428 2 ## lint-rpm.pl -- OpenPKG *.rpm File Checker
michael@428 3 ## Copyright (c) 2000-2012 OpenPKG GmbH <http://openpkg.com/>
michael@428 4 ##
michael@428 5 ## This software is property of the OpenPKG GmbH, DE MUC HRB 160208.
michael@428 6 ## All rights reserved. Licenses which grant limited permission to use,
michael@428 7 ## copy, modify and distribute this software are available from the
michael@428 8 ## OpenPKG GmbH.
michael@428 9 ##
michael@428 10 ## THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED
michael@428 11 ## WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
michael@428 12 ## MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
michael@428 13 ## IN NO EVENT SHALL THE AUTHORS AND COPYRIGHT HOLDERS AND THEIR
michael@428 14 ## CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
michael@428 15 ## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
michael@428 16 ## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
michael@428 17 ## USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
michael@428 18 ## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
michael@428 19 ## OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
michael@428 20 ## OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
michael@428 21 ## SUCH DAMAGE.
michael@428 22 ##
michael@428 23
michael@428 24 # Perl run-time requirement
michael@428 25 require 5;
michael@428 26 BEGIN {
michael@428 27 eval "use Getopt::Long; use IO;";
michael@428 28 if ($@) {
michael@428 29 print STDERR
michael@428 30 "lint-rpm: ERROR: This command requires a full-size Perl installation!\n" .
michael@428 31 "lint-rpm: HINT: Install OpenPKG \"perl\" package to use this command.\n";
michael@428 32 exit(1);
michael@428 33 }
michael@428 34 }
michael@428 35
michael@428 36 # OpenPKG instance prefix
michael@428 37 my $my_prefix = $ENV{'OPENPKG_PREFIX'};
michael@428 38 delete $ENV{'OPENPKG_PREFIX'};
michael@428 39
michael@428 40 # program information
michael@428 41 my $progname = "lint-rpm";
michael@428 42 my $progvers = "1.0.0";
michael@428 43
michael@428 44 # parameters (defaults)
michael@428 45 my $version = 0;
michael@428 46 my $verbose = 0;
michael@428 47 my $help = 0;
michael@428 48 my $check = 'all';
michael@428 49 my $tmpdir = ($ENV{TMPDIR} || $ENV{TEMPDIR} || "/tmp") . "/$progname";
michael@428 50 my $rpm = "$my_prefix/bin/openpkg rpm";
michael@428 51 my $rpm2cpio = "$my_prefix/bin/openpkg rpm2cpio";
michael@428 52
michael@428 53 # exception handling support
michael@428 54 $SIG{__DIE__} = sub {
michael@428 55 my ($err) = @_;
michael@428 56 $err =~ s|\s+at\s+.*||s if (not $verbose);
michael@428 57 print STDERR "$progname:ERROR: $err ". ($! ? "($!)" : "") . "\n";
michael@428 58 exit(1);
michael@428 59 };
michael@428 60
michael@428 61 # command line parsing
michael@428 62 Getopt::Long::Configure("bundling");
michael@428 63 my $result = GetOptions(
michael@428 64 'V|version' => \$version,
michael@428 65 'v|verbose' => \$verbose,
michael@428 66 'h|help' => \$help,
michael@428 67 'c|check=s' => \$check,
michael@428 68 't|tmpdir=s' => \$tmpdir,
michael@428 69 'r|rpm=s' => \$rpm,
michael@428 70 ) || die "option parsing failed";
michael@428 71 if ($help) {
michael@428 72 print "Usage: $progname [options] [RPMFILE ...]\n" .
michael@428 73 "Available options:\n" .
michael@428 74 " -v,--verbose enable verbose run-time mode\n" .
michael@428 75 " -h,--help print out this usage page\n" .
michael@428 76 " -c,--check=CHECKS select checks to perform (default='all')\n" .
michael@428 77 " -r,--rpm=FILE filesystem path to RPM program\n" .
michael@428 78 " -t,--tmpdir=PATH filesystem path to temporary directory\n" .
michael@428 79 " -V,--version print program version\n";
michael@428 80 exit(0);
michael@428 81 }
michael@428 82 if ($version) {
michael@428 83 print "OpenPKG $progname $progvers\n";
michael@428 84 exit(0);
michael@428 85 }
michael@428 86
michael@428 87 # verbose message printing
michael@428 88 sub msg_verbose {
michael@428 89 my ($msg) = @_;
michael@428 90 print STDERR "$msg\n" if ($verbose);
michael@428 91 }
michael@428 92
michael@428 93 # warning message printing
michael@428 94 sub msg_warning {
michael@428 95 my ($msg) = @_;
michael@428 96 print STDERR "$progname:WARNING: $msg\n";
michael@428 97 }
michael@428 98
michael@428 99 # error message printing
michael@428 100 sub msg_error {
michael@428 101 my ($msg) = @_;
michael@428 102 print STDERR "$progname:ERROR: $msg\n";
michael@428 103 }
michael@428 104
michael@428 105 # determine check list
michael@428 106 my @check_list = (qw(
michael@428 107 layout
michael@428 108 attrib
michael@428 109 content
michael@428 110 ));
michael@428 111 my @checks = ();
michael@428 112 if ($check eq 'all') {
michael@428 113 @checks = @check_list;
michael@428 114 }
michael@428 115 else {
michael@428 116 foreach my $c (split(/,/, $check)) {
michael@428 117 if (not grep(/^$c$/, @check_list)) {
michael@428 118 die "invalid check \"$c\"";
michael@428 119 }
michael@428 120 push(@checks, $c);
michael@428 121 }
michael@428 122 }
michael@428 123
michael@428 124 # global return code
michael@428 125 $main::GRC = 0;
michael@428 126
michael@428 127 # environment preparation
michael@428 128 system("rm -rf $tmpdir");
michael@428 129 system("mkdir -p $tmpdir");
michael@428 130
michael@428 131 # iterate over all .rpm files
michael@428 132 foreach my $filename (@ARGV) {
michael@428 133 die "unable to open file \"$filename\" for reading"
michael@428 134 if (not -f $filename);
michael@428 135 my $info = &rpm_info($filename, \@checks);
michael@428 136 foreach my $check (@checks) {
michael@428 137 eval "\&check_$check(\$filename, \$info);";
michael@428 138 }
michael@428 139 &rpm_info_cleanup($filename, \@checks, $info);
michael@428 140 }
michael@428 141
michael@428 142 # environment cleanup
michael@428 143 system("rm -rf $tmpdir");
michael@428 144
michael@428 145 # die gracefully
michael@428 146 exit($main::GRC);
michael@428 147
michael@428 148 ## _________________________________________________________________
michael@428 149 ##
michael@428 150 ## COMMON SUBROUTINES
michael@428 151 ## _________________________________________________________________
michael@428 152 ##
michael@428 153
michael@428 154 sub lint_message {
michael@428 155 my ($type, $file, $msg) = @_;
michael@428 156 $file =~ s|^.+?/([^/]+)$|$1|s;
michael@428 157 printf(STDERR "%s:%s: %s: %s\n", $progname, $type, $file, $msg);
michael@428 158 }
michael@428 159
michael@428 160 sub lint_warning {
michael@428 161 my ($file, $msg) = @_;
michael@428 162 &lint_message("WARNING", $file, $msg);
michael@428 163 $main::GRC = 1 if ($main::GRC < 1);
michael@428 164 }
michael@428 165
michael@428 166 sub lint_error {
michael@428 167 my ($file, $msg) = @_;
michael@428 168 &lint_message("ERROR", $file, $msg);
michael@428 169 $main::GRC = 2 if ($main::GRC < 2);
michael@428 170 }
michael@428 171
michael@428 172 ## _________________________________________________________________
michael@428 173 ##
michael@428 174 ## RPM INFORMATION GATHERING
michael@428 175 ## _________________________________________________________________
michael@428 176 ##
michael@428 177
michael@428 178 sub rpm_info {
michael@428 179 my ($filename, $checks) = @_;
michael@428 180 my $info = {};
michael@428 181
michael@428 182 # query package name
michael@428 183 &msg_verbose("++ querying RPM package name");
michael@428 184 $info->{name} = `$rpm -qp --qf '%{NAME}' $filename`;
michael@428 185
michael@428 186 # query prefix
michael@428 187 &msg_verbose("++ querying RPM package installation prefix");
michael@428 188 $info->{prefix} = `$rpm -qp --qf '%{PREFIXES}' $filename`;
michael@428 189
michael@428 190 # query file listing
michael@428 191 &msg_verbose("++ querying RPM package file listing");
michael@428 192 my @list = `$rpm -qplv $filename`;
michael@428 193 my @config = `$rpm -qplc $filename`;
michael@428 194
michael@428 195 # process file listing
michael@428 196 $info->{ls} = {};
michael@428 197 foreach my $entry (@list) {
michael@428 198 if ($entry =~ m|^\(contains no files\)\s*$|s) {
michael@428 199 next;
michael@428 200 }
michael@428 201 elsif ($entry =~ m|^(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(.{12})\s+(.+)\s*$|s) {
michael@428 202 my ($perm, $links, $owner, $group, $size, $mtime, $path) = ($1, $2, $3, $4, $5, $6, $7);
michael@428 203 my $symlink = "";
michael@428 204 if ($path =~ m|^(\S+)\s+->\s+(\S+)$|) {
michael@428 205 ($path, $symlink) = ($1, $2);
michael@428 206 }
michael@428 207 $path =~ s|\s+$||s;
michael@428 208 my $config = 0;
michael@428 209 if (grep(m|^$path$|, @config)) {
michael@428 210 $config = 1;
michael@428 211 }
michael@428 212 $info->{ls}->{$path} = {
michael@428 213 'perm' => $perm,
michael@428 214 'links' => $links,
michael@428 215 'owner' => $owner,
michael@428 216 'group' => $group,
michael@428 217 'size' => $size,
michael@428 218 'time' => $mtime,
michael@428 219 'path' => $path,
michael@428 220 'symlink' => $symlink,
michael@428 221 'config' => $config,
michael@428 222 };
michael@428 223 }
michael@428 224 else {
michael@428 225 &lint_error($filename, "invalid file listing entry: \"$entry\"");
michael@428 226 }
michael@428 227 }
michael@428 228
michael@428 229 # unpacking files
michael@428 230 if (grep(/^content$/, @{$checks})) {
michael@428 231 &msg_verbose("++ unpacking RPM package files");
michael@428 232 $info->{root} = "$tmpdir/root";
michael@428 233 system("mkdir -p ".$info->{root});
michael@428 234 system("$rpm2cpio $filename | (cd ".$info->{root}." && cpio -idmu 2>/dev/null)");
michael@428 235 }
michael@428 236
michael@428 237 return $info;
michael@428 238 }
michael@428 239
michael@428 240 sub rpm_info_cleanup {
michael@428 241 my ($filename, $checks, $info) = @_;
michael@428 242
michael@428 243 if (grep(/^content$/, @{$checks})) {
michael@428 244 system("rm -rf ".$info->{root}) if ($info->{root} =~ m/^\/.+/ and -d $info->{root});
michael@428 245 }
michael@428 246 }
michael@428 247
michael@428 248 ## _________________________________________________________________
michael@428 249 ##
michael@428 250 ## CHECK "layout": file path layout
michael@428 251 ## _________________________________________________________________
michael@428 252 ##
michael@428 253
michael@428 254 sub check_layout {
michael@428 255 my ($rpm, $info) = @_;
michael@428 256
michael@428 257 # no need to check 'openpkg' package because it
michael@428 258 # has a hard-coded file list!
michael@428 259 return if ($rpm =~ m|^(.+?/)?openpkg-\d[^/]+$|);
michael@428 260
michael@428 261 # check prefix
michael@428 262 if ($info->{prefix} !~ m|^/.+$|) {
michael@428 263 &lint_error($rpm, "invalid installation prefix ".$info->{prefix}.
michael@428 264 " (expected to match \"^/.+\$\")");
michael@428 265 return;
michael@428 266 }
michael@428 267
michael@428 268 # check top-level path (all-in-one)
michael@428 269 my @topdirs = (qw(
michael@428 270 bin cgi etc include info lib libexec
michael@428 271 local man pub sbin share usr var
michael@428 272 ));
michael@428 273 my $topdirs = "{".join(",", @topdirs)."}";
michael@428 274 if (not keys(%{$info->{ls}})) {
michael@428 275 &lint_error($rpm, "invalid empty package (expected at least one file)");
michael@428 276 return;
michael@428 277 }
michael@428 278 foreach my $path (keys(%{$info->{ls}})) {
michael@428 279 my $ok = 0;
michael@428 280 foreach my $topdir (@topdirs) {
michael@428 281 my $prefix = quotemeta($info->{prefix} . "/" . $topdir);
michael@428 282 if ($path =~ m/^$prefix$/ && $rpm !~ m|^openpkg-\d+|) {
michael@428 283 &lint_error($rpm, "top-level directory \"$topdir\" provided" .
michael@428 284 " (expected none except for 'openpkg' package)");
michael@428 285 }
michael@428 286 if ($path =~ m/^$prefix/) {
michael@428 287 $ok = 1;
michael@428 288 last;
michael@428 289 }
michael@428 290 }
michael@428 291 if (not $ok) {
michael@428 292 &lint_error($rpm, "invalid top-level directory in path \"$path\"".
michael@428 293 " (expected one of $topdirs)");
michael@428 294 }
michael@428 295 }
michael@428 296
michael@428 297 # check for second-level path (all-in-one)
michael@428 298 my @topdirs_subdir_no = (qw(bin cgi info sbin));
michael@428 299 my @topdirs_subdir_yes = (qw(etc libexec share var));
michael@428 300 foreach my $path (keys(%{$info->{ls}})) {
michael@428 301 foreach my $topdir (@topdirs_subdir_yes) {
michael@428 302 my $prefix = quotemeta($info->{prefix} . "/" . $topdir);
michael@428 303 if ($path =~ m/^$prefix\/[^\/]+$/) {
michael@428 304 if ($info->{ls}->{$path}->{perm} !~ m|^d|) {
michael@428 305 &lint_error($rpm, "invalid positioned file \"$path\" under topdir \"$topdir\" (expected directory)");
michael@428 306 }
michael@428 307 }
michael@428 308 }
michael@428 309 foreach my $topdir (@topdirs_subdir_no) {
michael@428 310 my $prefix = quotemeta($info->{prefix} . "/" . $topdir);
michael@428 311 if ($path =~ m/^$prefix\/[^\/]+$/) {
michael@428 312 if ($info->{ls}->{$path}->{perm} =~ m|^d|) {
michael@428 313 &lint_error($rpm, "invalid positioned directory \"$path\" under topdir \"$topdir\" (expected file)");
michael@428 314 }
michael@428 315 }
michael@428 316 }
michael@428 317 }
michael@428 318
michael@428 319 # check "bin" and "sbin" directories
michael@428 320 foreach my $path (keys(%{$info->{ls}})) {
michael@428 321 foreach my $topdir (qw(bin sbin)) {
michael@428 322 my $prefix = quotemeta($info->{prefix} . "/" . $topdir);
michael@428 323 if ($path =~ m/^$prefix\/(.+)$/) {
michael@428 324 my $file = $1;
michael@428 325 if ($file =~ m|^[^/]+\.[^/.]+$|) {
michael@428 326 &lint_warning($rpm, "strange executable filename \"$path\" containing an extension (expected no extension)");
michael@428 327 }
michael@428 328 my $perm = $info->{ls}->{$path}->{'perm'};
michael@428 329 if ($perm =~ m|^-| && $perm !~ m|^-[-r][-w][sx][-r][-w][-sx][-r][-w][-tx]$|) {
michael@428 330 &lint_error($rpm, "non-executable file \"$path\" (with permissions \"$perm\" under topdir \"$topdir\" (expected to be executable)");
michael@428 331 }
michael@428 332 }
michael@428 333 }
michael@428 334 }
michael@428 335
michael@428 336 # check for symbolic link targets (outside absolute, dangling)
michael@428 337 foreach my $path (keys(%{$info->{ls}})) {
michael@428 338 my $symlink = $info->{ls}->{$path}->{'symlink'};
michael@428 339 if ($symlink ne '') {
michael@428 340 # check for outside absolute target
michael@428 341 my $prefix = quotemeta($info->{prefix});
michael@428 342 if ($symlink =~ m|^/.*| and $symlink !~ m|^$prefix|s) {
michael@428 343 &lint_warning($rpm, "symbolic link \"$path\" points to absolute path \"$symlink\" outside prefix \"$info->{prefix}\" (expected it to be under prefix only)");
michael@428 344 }
michael@428 345 # check for dangling target
michael@428 346 my $resolved = &resolve($info, $path);
michael@428 347 sub resolve {
michael@428 348 my ($info, $path) = @_;
michael@428 349 if (not defined($info->{ls}->{$path})) {
michael@428 350 return $path;
michael@428 351 }
michael@428 352 my $symlink = $info->{ls}->{$path}->{'symlink'};
michael@428 353 if ($symlink eq '') {
michael@428 354 return $path;
michael@428 355 }
michael@428 356 else {
michael@428 357 my $resolved;
michael@428 358 if ($symlink =~ m|^/|) {
michael@428 359 $resolved = $symlink;
michael@428 360 }
michael@428 361 else {
michael@428 362 $resolved = $path;
michael@428 363 $resolved =~ s|/[^/]+$||s;
michael@428 364 $resolved .= "/" . $symlink;
michael@428 365 $resolved =~ s|/{2,}|/|sg;
michael@428 366 $resolved =~ s|/\.(?=/)||sg;
michael@428 367 $resolved =~ s|/\.$||sg;
michael@428 368 1 while ($resolved =~ s|/[^/]+/\.\./|/|s);
michael@428 369 $resolved =~ s|/[^/]+/\.\.$||s;
michael@428 370 $resolved =~ s|(.)/$|$1|s;
michael@428 371 }
michael@428 372 return &resolve($info, $resolved);
michael@428 373 }
michael@428 374 }
michael@428 375 if (not defined($info->{ls}->{$resolved})) {
michael@428 376 &lint_error($rpm, "symbolic link \"$path\" points to not existing target path \"$resolved\" (expected existing target path)");
michael@428 377 }
michael@428 378 }
michael@428 379 }
michael@428 380 }
michael@428 381
michael@428 382 ## _________________________________________________________________
michael@428 383 ##
michael@428 384 ## CHECK "attrib": file attributes
michael@428 385 ## _________________________________________________________________
michael@428 386 ##
michael@428 387
michael@428 388 sub check_attrib {
michael@428 389 my ($rpm, $info) = @_;
michael@428 390
michael@428 391 # check for empty files
michael@428 392 foreach my $path (keys(%{$info->{ls}})) {
michael@428 393 if ( $info->{ls}->{$path}->{'size'} == 0
michael@428 394 and $info->{ls}->{$path}->{'perm'} !~ m|^d|
michael@428 395 and $path !~ m/^.*\/lib\/perl\/.+\.bs$/
michael@428 396 and $rpm !~ m/\bopenpkg-[0-9]+/) {
michael@428 397 &lint_warning($rpm, "empty file \"$path\" found (expected no empty files)");
michael@428 398 }
michael@428 399 }
michael@428 400
michael@428 401 # check for %config flagged files
michael@428 402 my $prefix = quotemeta($info->{'prefix'});
michael@428 403 my $name = quotemeta($info->{'name'});
michael@428 404 foreach my $path (keys(%{$info->{ls}})) {
michael@428 405 my $config = $info->{ls}->{$path}->{'config'};
michael@428 406 my $path = $info->{ls}->{$path}->{'path'};
michael@428 407 if ($rpm !~ m/\bopenpkg-[0-9]+/ and $config and $path !~ m/^$prefix\/etc\/($name\d*|fsl)\/.+/) {
michael@428 408 &lint_warning($rpm, "non-config file \"$path\" flagged as \%config (expected only \"$prefix/etc/$name/*\" files to be \%config files)");
michael@428 409 }
michael@428 410 if ($config and $path =~ m/^$prefix\/s?bin\/[^\/]+$/) {
michael@428 411 &lint_error($rpm, "executable file \"$path\" flagged as \%config");
michael@428 412 }
michael@428 413 if ($config and $path =~ m/^$prefix\/etc\/rc\.d\/rc\.$name$/) {
michael@428 414 &lint_error($rpm, "run-command file \"$path\" flagged as \%config");
michael@428 415 }
michael@428 416 }
michael@428 417
michael@428 418 # check for permissions
michael@428 419 foreach my $path (keys(%{$info->{ls}})) {
michael@428 420 my $perm = $info->{ls}->{$path}->{'perm'};
michael@428 421 if ($path =~ m/^$prefix\/(bin|sbin)\/[^\/]+$/) {
michael@428 422 if ($perm !~ m|^[^d]..[xs]..[xs]..[xt]$|) {
michael@428 423 &lint_warning($rpm, "executable file \"$path\" has permissions \"$perm\" only (expected it to be executable by everyone)");
michael@428 424 }
michael@428 425 }
michael@428 426 }
michael@428 427
michael@428 428 # TODO: user/group?
michael@428 429 }
michael@428 430
michael@428 431 ## _________________________________________________________________
michael@428 432 ##
michael@428 433 ## CHECK "content": file content
michael@428 434 ## _________________________________________________________________
michael@428 435 ##
michael@428 436
michael@428 437 sub check_content {
michael@428 438 my ($rpm, $info) = @_;
michael@428 439
michael@428 440 # TODO: stripped (file)
michael@428 441 # TODO: syslibs (ldd)
michael@428 442 # TODO: hard-coded paths pointing outside instance
michael@428 443 # TODO: sanity check for shebang-lines
michael@428 444 # TODO: contained temporary path
michael@428 445 # TODO: bad-files: .po
michael@428 446 }
michael@428 447

mercurial