#!/usr/bin/perl 

# Copyright 2004  Mark Tucker (mark@tucker.net)
# All rights reserved.
#
# Redistribution and use of this script, with or without modification, is
# permitted provided that the following conditions are met:
#
# 1. Redistributions of this script must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#
#  THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
#  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
#  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO
#  EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
#  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
#  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
#  OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
#  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
#  OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
#  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

## Adapted for Salix by Richard Lapointe = April 10, 2010
## slk-pkgcheck version 1.1 - June 21, 2010

# pkgcheck.pl
#
#
# VERSION 0.7
#
# Script adapted from slackware's pkgcheck.pl (by Mark Tucker)
# to check a Salix package for common (and, hopefully, uncommmon
# errors. The following checks are made:
#  - package naming.
#  - existance of md5 and its validity (or ty to create it)
#  - set UID files
#  - "./" directory exists in package
#  - the "install" dir exists
#  - slack-desc file exists.
#  - contents of slack-desc file (checks that there is something there)
#  - slkbuild file exists.
#  - contents of slkbuild file (author, source location, ...)
#  - non-root owned files
#  - bin directories and files are group "root" and good dirctories
#  - bin files are executable
#  - bin are stripped
#  - zero length files
#  - non-readable files
#  - non-readable directories
#  - existance of doc directory (and that it matches the package name)
#  - documentation directory at /usr/share/doc
#  - empty documentation directory
#  - man directory in /usr/share/man
#  - non-compressed man pages
#  - non-compressed info pages
#  - existance of info dir or dir.gz file
#  - existance of /usr/etc directory
#  - existance of /usr/local directory
#  - abnormally large files

#  Comments, problems or suggestions can be sent to mark@tucker.net
#

#  CHANGES:
#     Jul 23, 2004 - initial creation
#     Sep 8, 2004  - added "noarch" as a valid arch type
#                  - added check for /usr/share/man
#                  - added check for /usr/share/doc
#     Jun 3, 2006  - add Zenwalk specific stuffs (zenbuild, .desktop, ...)
#     Jun 9, 2006  - add md5 check and creation
#     Nov 20, 2006 - changes due to new RELEASE.BUILD packages name
#     Feb 8, 2007  - RELEASE.BUILD error is now warning(kernel version allowed)
#     Sep 13, 2007 - bugfix in md5,.desktop, build-pkg.sh checks, added /usr/share/pixmaps test
#     Apr 21, 2009 - added tlz support
#	  Apr 10, 2010 - adapted for Salix 
#				   - script name changed to slk-pkgcheck.pl 	
#				   - removed check for build-$pkg_name.sh 
#				   - allow for alpha characters in pgkver (i.e  -1rl.txz) 	
##	  Apr 17, 2010 - bugfix in arch test
##	  Jun 21, 2010 - bug fix in arch test, x86_64 now valid value

##  Version 1.2
##	  Jan 05, 2011 - added arm as a valid arch		 
	
				
#============================================================================


# set vars and paths
@bindirs = ('usr/bin/', 'bin/', 'sbin/', 'usr/sbin/', 'usr/X11R6/bin/');

$debug = 0;

# parse arguments (if any)
if(! defined @ARGV) {
    print "You must provide the name of a package to test\n";
    print "\nUsage:\n\t$0 pkg_name-ver-arch-rel.t[glxb]z\n\n";
    exit;
}else{
    chomp($PKG = $ARGV[0]);
}




###################################################
# check package naming
###################################################
chomp($filename = `basename $PKG`);
print "Checking package: $filename\n";
@pkg_nam = split(/-/, $filename);

$softname_and_ver = join('-', @pkg_nam[0..($#pkg_nam - 2)]);

#----------------
# ends w/ .t[glxb]z
#----------------
print "Checking file name extention (.t[glxb]z)... ";
if($filename !~ /\.t[glxb]z$/) {
    print "\n\nERROR: package is not a compressed tar archive or is not "
	."properly named. Exiting...\n";
    exit;
}else{
    print "OK\n";
}

#----------------
# has #xxx release name
#----------------
print "Checking package name release... ";
# softname-$softversion-$arch-$packageversion
$testok = 1;
($pkg_rel, $junk) = split(/\./, $pkg_nam[$#pkg_nam]);
if($pkg_rel !~ /^[0-9]/) {
    print "\nERROR: package release must indicate a number value.\n";
    print "$pkg_rel" if ($debug);
    $testok = 0;
}
# print "pkg rel : [$pkg_rel]";
# print "\n a-z [".$pkg_rel !~ /[a-zA-Z]$/."]";
if($pkg_rel !~ /[0-9]/ ) {
    print "\nERROR: package release must contain only numbers.\n";
    print "$pkg_rel" if ($debug);
    $testok = 0;
}

if(!defined($junk) || $junk !~ /[0-9a-zA-Z]/ ) {
    &err_warn("\nERROR: package build must exists and be a number.");
    print "$pkg_rel" if ($debug);
    $testok = 2;
}

if($testok == 0) {
    exit;
}else{
    print "$pkg_rel\n";
}

#----------------
# check for valid arch (i386 -> i686, x86_64, arm or noarch)
#----------------
print "Checking package arch... ";
$pkg_arch = $pkg_nam[($#pkg_nam - 1)];
if($pkg_arch =~ /(i[3456]86)|noarch|x86_64|arm/) {
    print "$pkg_arch\n";
}else{
    &err_fatal("\nERROR: package arch, $pkg_arch, is not a valid value.");
}

#----------------
# store version field
#----------------
print "Checking package version... ";
$pkg_ver = $pkg_nam[($#pkg_nam - 2)];
print "$pkg_ver\n";

#----------------
# store package name
#----------------
print "Checking package name... ";
if($#pkg_nam > 3) {
    $pkg_name = join('-', @pkg_nam[0..($#pkg_nam - 3)]);
}else{
    $pkg_name = $pkg_nam[0];
}
print "$pkg_name\n";

#----------------
# store package-ver name
#----------------
$pkg_name_ver = "$pkg_name-$pkg_ver";


#----------------
# check the md5 file (or create it)
#----------------
$testok = 1;
print "Checking md5 file... ";
$md5_file= $PKG;
$md5_file=~ s/\.t[glxb]z$/\.md5/;
$cheksum_calc=`md5sum $PKG | cut -f1 -d ' '`;

if (-r $md5_file)
{
    $cheksum_file=`cat $md5_file | cut -f1 -d ' '`;
    if ($cheksum_calc ne $cheksum_file)
    {
	&err_warn("\nERROR: Wrong md5 : $cheksum_file != $cheksum_calc");
	$testok = 0;
    }
}else{
    print "md5 not readable, script is trying to create it ...";
    `md5sum $PKG > $md5_file`;
    if (! -r $md5_file)
    {
	&err_warn("\nERROR: Can't create md5 file");
	$testok = 0;
    }
}

print "OK\n" if ($testok == 1);


###################################################
# read the contents of the package
###################################################
print "Reading the contents of the package... \n";
chomp(@package = `tar -tvf $PKG`);

#--------------------------------------------------
# create hashes for each line element
#--------------------------------------------------
foreach $k (@package) {
#    print "$k\n";
    my @line = split(/\s+/, $k);
    $name = $line[$#line];
    push(@all_names, $name);

    # create arrays by file type
    if ($k =~ /^d/) {push(@dirs, $name);}
    elsif ($k =~ /^-/) {push(@files, $name);}
    elsif ($k =~ /^l/) {push(@links, $name);}
    else {next}

    # store permissions by name
    $perms{$name} = $line[0];
    $filetype = substr($perms{$name}, 0, 1);
    $perms_owner{$name} = substr($perms{$name}, 1, 3);
    $perms_group{$name} = substr($perms{$name}, 4, 3);
    $perms_other{$name} = substr($perms{$name}, 7, 3);
   
    # store owner,group by name
    ($owner{$name},$group{$name}) = split(/\//, $line[1]);

    # store file/dir size by name
    $size{$name} = $line[2];
}


###################################################
# specific checks....
###################################################
#--------------------------------------------------
# check for suid files
#--------------------------------------------------
print "Checking setuid files... ";
$testok = 1;
foreach $k (@files) {
    if($perms_owner{$k} =~ /s$/) {
	&err_fatal("Permissions for $k are SUID");
	$testok = 0;
    }
}
print "OK\n" if($testok == 1);

#--------------------------------------------------
# check for "./" dir
#--------------------------------------------------
print "Checking for proper root directory... ";
$testok = 0;
foreach $k (@dirs) {
    if($k eq './') {
	print "OK\n";
	$testok = 1;
	last;
    }
}
if($testok == 0) {
    print "Error\n";
    &err_fatal("'./' directory entry missing from package.");
}


#--------------------------------------------------
# check for ./install dir
#--------------------------------------------------
print "Checking for './install' directory... ";
$testok = 0;
foreach $k (@dirs) {
    if($k eq 'install/') {
	print "OK\n";
	$testok = 1;
	last;
    }
}
if($testok == 0) {
    &err_fatal("'install/' directory entry missing from package.");
}


#--------------------------------------------------
# check for slack-desc file
#--------------------------------------------------
print "Checking for slack-desc file... ";
$testok = 0;
foreach $k (@files) {
    if($k eq 'install/slack-desc') {
	print "OK\n";
	$testok = 1;
    }
}
if($testok == 0) {
    &err_fatal("'slack-desc' file is missing from package.");
}else{
#--------------------------------------------------
# check slack-desc file contents
#--------------------------------------------------
    print "Checking contents of slack-desc file... ";
    $testok = 0;

    #  dump contents to array
    chomp(@slkdsc = `tar -xOf $PKG install/slack-desc`);

    #--------------------
    # check for pkg_name in file
    # There should be more than 1 line starting with the package name.
    #--------------------
    foreach $j (@slkdsc) {
	if($j =~ /^$pkg_name:/) {
	    print "$j" if ($debug);
	    $count++;
	}
    }
    if($count != 0) {
	$testok = 1;
    }else{
	&err_warn("No lines in slack-desc match the package name, $pkg_name");
    }
    #--------------------
    # check for description lines
    # This just checks that there are at least 2 lines of description which
    # are more than 5 characters in lenght.
    #--------------------
    $count = 0;
    foreach $j (@slkdsc) {
	print "$j" if ($debug);
	next if($j !~ /^$pkg_name:/);
	$descr = substr($j, (length($pkg_name) + 2));
	$count++ if(length($descr) > 5);
    }
    if($count > 2) {
	$testok = 1;
    }else{
	&err_warn("slack-desc file seems to be a bit sparse, $count lines.");
    }


    print "OK\n" if($testok == 1);
}

#--------------------------------------------------
# check for SLKBUILD file
#--------------------------------------------------
print "Checking for SLKBUILD file... ";
$testok = 0;
$slkbuild_file = "";
foreach $k (@files) {
    if($k =~ /usr\/src\/$softname_and_ver\/build-$pkg_name.sh/i) {
	$testok = 1;
	$slkbuild_file = $k;
	last;
    }
}
if($testok == 0) {
#    &err_fatal("'build-$pkg_name.sh' file is missing from package.");
#}else{
#    print "OK\n";
#--------------------------------------------------
# check SLKBUILD file contents
#--------------------------------------------------
    $count = 0;
    print "Checking contents of slkbuild file... ";
    $testok = 0;

    #  dump contents to array
    chomp(@zbuild = `tar -xOf $PKG $slkbuild_file`);

    #--------------------
    # check for "source location" in file
    #--------------------
    foreach $j (@zbuild) {
	if($j =~ /source location|source|SRCURL|URL\s*:\s+http:\/\//i) {
	    print "$j" if ($debug);
	    $count++;
	    last
	}
    }
    if($count != 0) {
	$testok += 1;
    }else{
	&err_warn("No lines in build-$pkg_name.sh have 'source location:'");
    }
    #--------------------
    # check for author line
    #--------------------
    $count = 0;
    foreach $j (@zbuild) {
	if($j =~ /author|creator|maintainer\s*:\s.+/i) {
	    print "$j" if ($debug);
	    $count++;
	    last
	}
    }
    if($count != 0) {
	$testok += 1;
    }else{
	&err_warn("build-$pkg_name.sh file seems to be created by nobody.");
    }
}
print "OK\n" if($testok == 2);


#--------------------------------------------------
# check for icon directory
#--------------------------------------------------
print "Checking for icon directory... ";
$testok = 1;
foreach $k (@files) {
    if(lc($k) =~ /usr\/share\/pixmaps/) {
 	&err_warn("usr/share/pixmaps exists, be sure to put menu icons in usr/share/icons/hicolor/48x48/apps (for png) or usr/share/icons/hicolor/scalable (for svg).\n And try to put internal app icons in /usr/share/app-name/some-directory instead of usr/share/pixmaps");
	$testok = 0;
	last;
    }
}
print "OK\n" if($testok == 1);



#--------------------------------------------------
# check for .desktop file
#--------------------------------------------------
print "Checking for $pkg_name.desktop file... ";
$testok = 0;
$desktop_name="";
foreach $k (@files) {
    if(lc($k) =~ /usr\/share\/applications\/$pkg_name\.desktop|usr\/share\/applications\/.+\/$pkg_name\.desktop/i) {
	print "OK\n";
	$testok = 1;
	$desktop_name=$k;
	last;
    }
}
if($testok == 0) {
    &err_warn("'$pkg_name.desktop' file is missing from package.");
}else {
#--------------------------------------------------
# check .desktop file contents
#--------------------------------------------------
    print "Checking contents of $pkg_name.desktop file... ";
    $testok = 0;
    $count = 0;

    #  dump contents to array
    chomp(@dtop = `tar -xOf $PKG $desktop_name`);

    #--------------------
    # check for [Desktop Entry] in file
    #--------------------
    if ($dtop[0] =~ /\[Desktop Entry\]/) {
	$testok = 1;
    }else{
	&err_fatal("[Desktop Entry] isn't $pkg_name.desktop's first line");
    }

    foreach $j (@dtop) {
	if(lc($j) =~ /^icon=\/[:alpha:]\/[:alpha:]/) {
	    print "$j" if ($debug);
	    $count++;
	    last;
	}
    }
    if($count == 0) {
	$testok = 1;
    }else{
	&err_warn("$pkg_name.desktop icon is abolute path");
    }
    print "OK\n" if($testok == 1);
}


#--------------------------------------------------  
# check for non-root owned files or dirs (report error)
#--------------------------------------------------
print "Checking for non-root owned files and dirs... ";
$testok = 1;
foreach $k (@all_names) {
    if($owner{$k} ne 'root') {
	&err_fatal("$k not owned by the root user");
	$testok = 0;
    }
}
print "OK\n" if($testok == 1);
#--------------------------------------------------
# group=bin for /usr/bin, /bin, /sbin, /usr/sbin, and /usr/X11R6/bin 
#--------------------------------------------------
print "Checking group for bin directories... ";
$testok = 1;
foreach $k (@all_names) {
    foreach $j (@bindirs) {
	if(($k =~ /^$j/) && ($group{$k} ne 'root')) {
	    $testok = 0;
	    &err_warn("$k should have a group of 'root'");
	}
    }
}
print "OK\n" if($testok == 1);

#--------------------------------------------------
# check bin dirs for non-executable files
#--------------------------------------------------
print "Checking bin files for execute permissions ... ";
$testok = 1;

foreach $k (@files) {
    foreach $j (@bindirs) {
	if($k =~ /^$j/) {
	    $count = 0;
	    $count++ if($perms_owner{$k} =~ /x$/);
	    $count++ if($perms_group{$k} =~ /x$/);
	    $count++ if($perms_other{$k} =~ /x$/);
	    if($count == 0) {
		&err_warn("No execute permissions for $k");
		$testok = 0;
	    }
	}
    }
}
print "OK\n" if($testok == 1);

#--------------------------------------------------
# check for zero length files
#--------------------------------------------------
print "Checking zero length files... ";
$testok = 1;
foreach $k (@files) {
    if($size{$k} == 0) {
	if($k =~ /^install\//) {
	    &err_warn("file $k has zero length, using checkinstall, eh?");
	}else{
	    &err_suggest("file $k has zero length, this could be a problem.");
	}
	$testok = 0;
    }
}
print "OK\n" if($testok == 1);
	
#--------------------------------------------------
# check for non-readable files
#--------------------------------------------------
print "Checking non readable files... ";
$testok = 1;
foreach $k (@files) {
    $count = 0;
    $count++ if($perms_owner{$k} !~ /^r/);
    $count++ if($perms_group{$k} !~ /^r/);
    $count++ if($perms_other{$k} !~ /^r/);
    if($count != 0) {
	&err_suggest("File $k has no read permssions");
	$testok = 0;
    }
}
print "OK\n" if($testok == 1);


#--------------------------------------------------
# check for non-readable directories
#--------------------------------------------------
print "Checking non readable directories... ";
$testok = 1;
foreach $k (@dirs) {
    $count = 0;
    $count++ if($perms_owner{$k} !~ /^r.x$/);
    $count++ if($perms_group{$k} !~ /^r.x$/);
    $count++ if($perms_other{$k} !~ /^r.x$/);
    if($count != 0) {
	&err_suggest("Directory $k has no read permssions");
	$testok = 0;
    }
}
print "OK\n" if($testok == 1);

#--------------------------------------------------
# check for usr/doc/pkg_ver 
#--------------------------------------------------
print "Checking for documentation directory... ";
$testok = 0;
foreach $k (@dirs) {
    if(($k =~ /usr\/doc\/$pkg_name_ver/) ||
	($k =~ /usr\/share\/doc\/$pkg_name_ver/)) {
	$testok = 1;
    }
}
if($testok == 0) {
    &err_fatal("Missing documentation directory /usr/doc/$pkg_name_ver");
}else{
    print "OK\n";
}

#--------------------------------------------------
# check for usr/doc/pkg_ver or usr/share/doc/pkg_ver
#--------------------------------------------------
print "Checking for documentation directory in /usr/share... ";
$testok = 1;
foreach $k (@dirs) {
    if($k =~ /usr\/share\/doc\/$pkg_name_ver/) {
	$testok = 0;
    }
}
if($testok == 0) {
    &err_warn("Found documentation directory /usr/share/doc/, should be /usr/doc/");
}else{
    print "OK\n";
}


#--------------------------------------------------
# check for files in usr/doc/pkg_ver or usr/share/doc/pkg_ver
#--------------------------------------------------
print "Checking for empty documentation directory... ";
$testok = 0;
$count = 0;
foreach $k (@files) {
    $count++ if($k =~ /usr\/doc\/$pkg_name_ver/);
    $count++ if($k =~ /usr\/share\/doc\/$pkg_name_ver/);
}
if($count == 0) {
    &err_fatal("There are no files in usr/doc/$pkg_name_ver");
}else{
    print "OK\n";
}

#--------------------------------------------------
# check for usr/share/man directory
#--------------------------------------------------
print "Checking for /usr/share/man... ";
$testok = 1;
foreach $k (@dirs) {
    if($k =~ /usr\/share\/man/) {
	$testok = 0;
    }
}
if($testok == 0) {
    &err_warn("Man directory found in /usr/share/man - Would be better as /usr/man");
}else{
    print "OK, not found.\n";
}


#--------------------------------------------------
# check for gzipped man pages
#--------------------------------------------------
print "Checking non-compressed man pages... ";
$testok = 1;
foreach $k (@files) {
    if($k =~ /\/man\/man.*[1-9]$/) {
	&err_warn("man page $k should be compressed");
	$testok = 0;
    }
}
print "OK\n" if($testok == 1);

#--------------------------------------------------
# check for gzipped info pages
#--------------------------------------------------
print "Checking non-compressed info pages... ";
$testok = 1;
foreach $k (@files) {
    if(($k =~ /usr\/info\//) && ($k !~ /gz$/) &&
	($k !~ /dir/)) {
	&err_warn("info page $k should be compressed");
	$testok = 0;
    }
}
print "OK\n" if($testok == 1);

#--------------------------------------------------
# check for  info dir or dir.gz file (report error)
#--------------------------------------------------
print "Checking for info dir or dir.gz... ";
$testok = 1;
foreach $k (@files) {
    if($k =~ /info\/dir/) {
	&err_suggest("File $k will overwrite the user's info dir file.");
	$testok = 0;
    }
}
print "OK\n" if($testok == 1);
    
#--------------------------------------------------
# check for /usr/etc dir and files (report error)
#--------------------------------------------------
print "Checking for usr/etc/ directory... ";
$testok = 1;
foreach $k (@dirs) {
    if($k =~ /^usr\/etc\//) {
	&err_fatal("Directory $k should not exist, use /etc.");
	$testok = 0;
    }
}
print "OK\n" if($testok == 1);

#--------------------------------------------------
# check for /usr/local/ dir (report error)
#--------------------------------------------------
print "Checking for usr/local/ directory... ";
$testok = 1;
foreach $k (@dirs) {
    if($k =~ /^usr\/local\//) {
	&err_fatal("Directory $k should not exist.");
	$testok = 0;
    }
}
print "OK\n" if($testok == 1);

#--------------------------------------------------
# check for unusually large files..
#--------------------------------------------------
print "Checking for abnormally large files... ";
$testok = 1;
foreach $k (@files) {
    if($size{$k} > 10000000) {
	&err_suggest("File $k has a size of $size{$k}, you may want to run the 'strip' utility on it.");
	$testok = 0;
    }
}
print "OK\n" if($testok == 1);

#--------------------------------------------------
# stripping binaries
#--------------------------------------------------
print "Checking for unstripped bin... ";
$testok = 1;
foreach $k (@files) {
    if($k =~ /usr\/bin|usr\/sbin/)
    {
	`tar -xOf $PKG $k > bin_strip`;
	print "check file:[" .$k ."]\n" if ($debug != 0);
	if (`file bin_strip` !~ /script/)
	{
	    `strip -s bin_strip`;
	    $m=`ls -la bin_strip`;
	    `rm bin_strip`;
	    my @stripped = split(/\s+/, $m);
	    print "   size  : ". $size{$k}. "\n" if ($debug != 0);
	    print "   strip : ". $stripped[4]. "\n\n" if ($debug != 0);
	    if($size{$k} != $stripped[4]) {
		&err_warn("File $k hasn't been stripped.");
		$testok = 0;
	    }
	}
    }
}
print "OK\n" if($testok == 1);

###################################################
# report errors
###################################################
print "-" x 60 ."\n";

# print fatal errors
print "\nFATAL ERRORS:\n";
foreach $i (@fatal_errors) {
    print "\t$i\n";
}
    
# print warnings
print "\nWARNINGS:\n";
foreach $i (@warnings) {
    print "\t$i\n";
}

# print suggestions
print "\nSUGGESTIONS:\n";
foreach $i (@suggestions) {
    print "\t$i\n";
}

# print summary
print "-" x 60 ."\n";
print "SUMMARY for $filename:";
print "\n".($#fatal_errors + 1).
    " fatal errors.\n".($#warnings + 1)." warnings.\n".
    ($#suggestions + 1)." suggestions\n";
print "-" x 60 ."\n";
exit;
###################################################
# sub routines
###################################################
#==================================================
sub err_fatal {
    print "  Error.\n";
    push(@fatal_errors, $_[0]);
}
#==================================================
sub err_warn {
    print "  Warning.\n";
    push(@warnings, $_[0]);
}
#==================================================
sub err_suggest {
    print "OK.\n";
    push(@suggestions, $_[0]);
}
