# shared-libs -- lintian check script -*- perl -*-

# Copyright (C) 1998 Christian Schwarz
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, you can find it on the World Wide
# Web at http://www.gnu.org/copyleft/gpl.html, or write to the Free
# Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
# MA 02110-1301, USA.

package Lintian::shared_libs;
use strict;
use Dep;
use Tags;
use File::Basename;
use Util;

sub run {

my %ldso_dir = map { $_ => 1 }
    qw( lib
	lib32
	lib64
        usr/lib
	usr/lib32
	usr/lib64
        usr/lib/libg++-dbg
        usr/X11R6/lib/Xaw3d
        usr/local/lib
        usr/X11R6/lib
        usr/lib/libc5-compat
        lib/libc5-compat
	emul/ia32-linux/lib
	emul/ia32-linux/usr/lib
      );

# Libraries that should only be used in the presence of certain capabilities
# may be located in subdirectories of the standard ldconfig search path with
# one of the following names.
my %hwcap_dir = map { $_ => 1 }
    qw( i486 i586 i686 cmov tls );

# The following architectures should always have a STACK setting in shared
# libraries to disable executable stack.  Other architectures don't always add
# this section and therefore can't be checked.
my %stack_arches = map { $_ => 1 }
    qw( alpha
	amd64
	i386
	m68k
	powerpc
	s390
	sparc
      );

my $file;
my $must_call_ldconfig;
my $postrm = "control/postrm";
my $postinst = "control/postinst";
my $preinst = "control/preinst";
my $prerm = "control/prerm";
my $shlibs_control_file = "control/shlibs";
my %SONAME;
my %INTERP;
my %STACK;
my %TEXTREL;
my %objsomething;
my %sharedobject;
my %index_info;
my %link_info;
my @shlibs;
my @words;

# ---end-of-configuration-part---

my $pkg = shift;
my $type = shift;

# 1st step: get info about shared libraries installed by this package
open(IN, '<', "objdump-info")
    or fail("cannot find objdump-info for $type package $pkg");
while (<IN>) {
    chop;

    #skip blank lines
    next if m/^\s*$/o;

    if (m/^-- (\S+)\s*$/o) {
	$file = $1;
	$file =~ s,^(\./)?,,;
    } elsif (m/^\s*SONAME\s+(\S+)/o) {
	$SONAME{$file} = $1;
    } elsif (m/^\s*TEXTREL\s/o) {
        $TEXTREL{$file} = 1;
    } elsif (m/^\s*INTERP\s/) {
	$INTERP{$file} = 1;
    } elsif (m/^\s*STACK\s/) {
	$STACK{$file} = 0;
    } else {
        if (defined $STACK{$file} and $STACK{$file} eq 0) {
	    m/\sflags\s+(\S+)/o;
	    $STACK{$file} = $1;
	}
	else {
	    $objsomething{$file} = 1;
	}
    }
}
close(IN);

open (IN, '<', "file-info")
    or fail("cannot find file-info for $type package $pkg");
while (<IN>) {
    chomp;
    # with file names containing colons and spaces, we're fucked.
    m/^(?:\.\/)?(.+?):\s+(.*)$/o or fail("unrecognized file(1) output: $_");
    my ($file,$info) = ($1,$2);
    if ($info =~ m/^[^,]*\bELF\b/ && $info =~ m/shared object/) {
	$sharedobject{$file} = 1;
    }
}
close(IN);

# 2nd step: read package contents
my %files;
open(IN, '<', "index") or fail("cannot open index file index: $!");
while (<IN>) {
    chop;
    @words = split(/\s+/o, $_, 6);
    my $perm = $words[0];
    my $cur_file = $words[5];
    $cur_file =~ s,^(\./),,;
    $cur_file =~ s/ link to .*//;

    if ($perm =~ m/^l/) {
	my $link;
	($cur_file, $link) = split(' -> ', $cur_file);
	$link_info{$cur_file} = $link;
    }
    $index_info{$cur_file} = 1;
    $files{$cur_file} = $perm;
}

for (keys %files) {
    my ($cur_file, $perm) = ($_, $files{$_});

    # shared library?
    if (exists $SONAME{$cur_file} or 
	($link_info{$cur_file} and exists $SONAME{abs_path(dirname($cur_file)."/".$link_info{$cur_file})})) {
	# yes!!

	my ($real_file, $real_perm);
	if ($SONAME{$cur_file}) {
	    $real_file = $cur_file;
	    $real_perm = $perm;
	} else {
	    $real_file = abs_path(dirname($cur_file)."/".$link_info{$cur_file});
	    $real_perm = $files{$real_file} || $perm;
	}

	# Now that we're sure this is really a shared library, report on
	# non-PIC problems.
        if ($cur_file eq $real_file and $TEXTREL{$cur_file}) {
            tag "shlib-with-non-pic-code", "$cur_file";
        }

        # don't apply the permission checks to links
        # since this only results in doubled messages
        if ($cur_file eq $real_file) { 
            # executable?
            if ($real_perm =~ m/x/) {
                # yes.  But if the library has an INTERP section, it's designed
                # to do something useful when executed, so don't report an error.
                tag "shlib-with-executable-bit", $cur_file, sprintf("%04o",perm2oct($real_perm))
                    unless $INTERP{$real_file};
            } elsif ($real_perm ne '-rw-r--r--') {
                # bad permissions
                tag "shlib-with-bad-permissions", $cur_file, sprintf("%04o",perm2oct($real_perm));
            }
        }

	# Installed in a directory controlled by the dynamic linker?  We have
	# to strip off directories named for hardware capabilities.
	my $dirname = dirname($cur_file);
	my $last;
	do {
	    $dirname =~ s%/([^/]+)$%%;
	    $last = $1;
	} while ($last && $hwcap_dir{$last});
	$dirname .= "/$last" if $last;
	if (exists $ldso_dir{$dirname}) {
	    # yes! so postinst must call ldconfig
	    $must_call_ldconfig = $real_file;
	}

	# executable stack.  We can only warn about a missing section on some
	# architectures.  Only warn if there's an Architecture field; if
	# that's missing, we'll already be complaining elsewhere.
	if (exists $objsomething{$cur_file}) {
	    if (not defined $STACK{$cur_file}) {
		if (open(FH, '<', "fields/architecture")) {
		    my $arch = <FH>;
		    close FH;
		    chomp $arch;
		    tag "shlib-without-PT_GNU_STACK-section", $cur_file
			if $stack_arches{$arch};
		}
	    } elsif ($STACK{$cur_file} ne "rw-") {
		tag "shlib-with-executable-stack", $cur_file;
	    }
	}
    } elsif (exists $objsomething{$cur_file} &&
	     exists $ldso_dir{dirname($cur_file)} &&
	     exists $sharedobject{$cur_file}) {
	tag "sharedobject-in-library-directory-missing-soname", "$cur_file";
    }
}

close(IN);

# 3rd step: check if shlib symlinks are present and in correct order
for my $shlib_file (keys %SONAME) {
    # file found?
    if (not exists $index_info{$shlib_file}) {
	fail("shlib $shlib_file not found in package (should not happen!)");
    }

    my ($dir, $shlib_name) = $shlib_file =~ m,(.*)/([^/]+)$,;

    # not a public shared library, skip it
    next unless defined $ldso_dir{$dir};

    # symlink found?
    my $link_file = "$dir/$SONAME{$shlib_file}";
    if (not exists $index_info{$link_file}) {
	tag "ldconfig-symlink-missing-for-shlib", "$link_file $shlib_file $SONAME{$shlib_file}";
    } else {
	# $link_file really another file?
	if ($link_file eq $shlib_file) {
	    # the library file uses its SONAME, this is ok...
	} else {
	    # $link_file really a symlink?
	    if (exists $link_info{$link_file}) {
		# yes.

		# $link_file pointing to correct file?
		if ($link_info{$link_file} eq $shlib_name) {
		    # ok.
		} else {
		    tag "ldconfig-symlink-referencing-wrong-file", "$link_file -> $link_info{$link_file} instead of $shlib_name";
		}
	    } else {
		tag "ldconfig-symlink-is-not-a-symlink", "$shlib_file $link_file";
	    }

	    # symlink after shlib?
	    if ($index_info{$link_file} < $index_info{$shlib_file}) {
		tag "ldconfig-symlink-before-shlib-in-deb", "$link_file";
	    }
	}
    }

    # determine shlib link name (w/o version)
    $link_file =~ s/\.so.*$/.so/o;

    # -dev package?
    if ($pkg =~ m/\-dev$/o) {
	# yes!!

	# need shlib symlink
	if (not exists $index_info{$link_file}) {
	    tag "dev-pkg-without-shlib-symlink", "$shlib_file $link_file";
	}
    } else {
	# no.

	# shlib symlink may not exist.
	# if shlib doesn't _have_ a version, then $link_file and $shlib_file will
	# be equal, and it's not a development link, so don't complain.
	if (exists $index_info{$link_file} and $link_file ne $shlib_file) {
	    tag "non-dev-pkg-with-shlib-symlink", "$shlib_file $link_file";
	}
    }
}

# 4th step: check shlibs control file
my $version;
if (open (VERSION, '<', 'fields/version')) {
    $version = <VERSION>;
    close VERSION;
    chomp $version;
}
@shlibs = grep { !m,^lib/libnss_[^.]+\.so(\.[0-9]+)$, } keys %SONAME;
if ($#shlibs == -1) {
    # no shared libraries included in package, thus shlibs control file should
    # not be present
    if (-f $shlibs_control_file) {
	tag "pkg-has-shlibs-control-file-but-no-actual-shared-libs", "";
    }
} else {
    # shared libraries included, thus shlibs control file has to exist
    if (not -f $shlibs_control_file) {
	if ($type ne 'udeb') {
	    for my $shlib (@shlibs) {
		# skip it if it's not a public shared library
		next unless defined $ldso_dir{dirname($shlib)};
		tag "no-shlibs-control-file", "$shlib";
	    }
	}
    } else {
	my %shlibs_control_used;
	my %shlibs_control;
	my @shlibs_depends;
	open(SHLIBS, '<', $shlibs_control_file)
	    or fail("cannot open shlibs control file $shlibs_control_file for reading: $!");
	while (<SHLIBS>) {
	    chop;
	    next if m/^\s*$/ or /^#/;

	    # We exclude udebs from the checks for correct shared library
	    # dependencies, since packages may contain dependencies on
	    # other udeb packages.
	    my $udeb="";
	    $udeb = "udeb: " if s/^udeb:\s+//o;
	    @words = split(/\s+/o,$_);
	    my $shlibs_string = $udeb.$words[0].' '.$words[1];
	    if ($shlibs_control{$shlibs_string}) {
		tag "duplicate-entry-in-shlibs-control-file", $shlibs_string;
	    } else {
		$shlibs_control{$shlibs_string} = 1;
		push (@shlibs_depends, join (' ', @words[2 .. $#words]))
		    unless $udeb;
	    }
	}
	close(SHLIBS);
	my $shlib_name;
	for my $shlib (@shlibs) {
	    $shlib_name = $SONAME{$shlib};
	    # libfoo.so.X.X
	    if ($shlib_name =~ m/(.+)\.so\.(.*)$/) {
		$shlib_name = "$1 $2";
	    # libfoo-X.X.so
	    } elsif ($shlib_name =~ m/(.+)\-(\w[\w\.]*)\.so$/) {
		$shlib_name = "$1 $2";
	    }
	    $shlibs_control_used{$shlib_name} = 1;
	    $shlibs_control_used{"udeb: ".$shlib_name} = 1;
	    unless (exists $shlibs_control{$shlib_name}) {
		# skip it if it's not a public shared library
 		next unless defined $ldso_dir{dirname($shlib)};
		# no!!
		tag "shlib-missing-in-control-file", $shlib_name, 'for', $shlib;
	    }
	}
	for $shlib_name (keys %shlibs_control) {
	    tag "unused-shlib-entry-in-control-file", $shlib_name
		unless $shlibs_control_used{$shlib_name};
	}

	# Check that all of the packages listed as dependencies in the shlibs
	# file are satisfied by the current package or its Provides.
	# Normally, packages should only declare dependencies in their shlibs
	# that they themselves can satisfy.
	#
	# Deduplicate the list of dependencies before warning so that we don't
	# dupliate warnings.
	my $provides = $pkg . "( = $version)";
	if (open (PROVIDES, '<', 'fields/provides')) {
	    my $line = <PROVIDES>;
	    close PROVIDES;
	    chomp $line;
	    $provides .= ", $line";
	}
	$provides = Dep::parse($provides);
	my %seen;
	@shlibs_depends = grep { !$seen{$_}++ } @shlibs_depends;
	for my $depend (@shlibs_depends) {
	    unless (Dep::implies($provides, Dep::parse($depend))) {
		tag "shlibs-declares-dependency-on-other-package", $depend;
	    }
	}
    }
}

# 5th step: check symbols control file
if (open(IN, '<', 'control/symbols')) {
    my $version_wo_rev = $version;
    $version_wo_rev =~ s/^(.+)-([^-]+)$/$1/;
    my ($full_version_count, $full_version_sym) = (0, undef);
    my ($debian_revision_count, $debian_revision_sym) = (0, undef);
    while (<IN>) {
	next if not /^\s+(\S+)\s(\S+)(?:\s(\d+))?/;
	my ($sym, $v, $dep_order) = ($1, $2, $3);
	if (($v eq $version) and ($version =~ /-/)) {
	    $full_version_sym ||= $sym;
	    $full_version_count++;
	}
	if (($v =~ /-/) and (not $v =~ /~$/) and ($v ne $version_wo_rev)) {
	    $debian_revision_sym ||= $sym;
	    $debian_revision_count++;
	}
    }
    close IN;
    if ($full_version_count) {
	$full_version_count--;
	my $others = '';
	if ($full_version_count > 0) {
	    $others = " and $full_version_count others";
	}
	tag "symbols-file-contains-current-version-with-debian-revision",
	    "on symbol $full_version_sym$others"
    }
    if ($debian_revision_count) {
	$debian_revision_count--;
	my $others = '';
	if ($debian_revision_count > 0) {
	    $others = " and $debian_revision_count others";
	}
	tag "symbols-file-contains-debian-revision",
	    "on symbol $debian_revision_sym$others";
    }
}


# 6th step: check pre- and post- control files
if (-f $preinst) {
    local $_ = slurp_entire_file($preinst);
    if (/^[^\#]*\bldconfig\b/m) {
	tag "preinst-calls-ldconfig", ""
    }
}

my $we_call_postinst=0;
if (-f $postinst) {
    local $_ = slurp_entire_file($postinst);

    # Decide if we call ldconfig
    if (/^[^\#]*\bldconfig\b/m) {
	$we_call_postinst=1;
    }
}

if ($type eq 'udeb') {
    tag "udeb-postinst-must-not-call-ldconfig"
	if $we_call_postinst;
} else {
    tag "postinst-has-useless-call-to-ldconfig", ""
	if $we_call_postinst and not $must_call_ldconfig;
    tag "postinst-must-call-ldconfig", "$must_call_ldconfig"
	if not $we_call_postinst and $must_call_ldconfig;
}

if (-f $prerm) {
    local $_ = slurp_entire_file($prerm);
    if (/^[^\#]*\bldconfig\b/m) {
	tag "prerm-calls-ldconfig", "";
    }
}

if (-f $postrm) {
    local $_ = slurp_entire_file($postrm);

    # Decide if we call ldconfig
    if (/^[^\#]*\bldconfig\b/m) {
	tag "postrm-has-useless-call-to-ldconfig", ""
	    unless $must_call_ldconfig;
    } else {
	tag "postrm-should-call-ldconfig", "$must_call_ldconfig"
	    if $must_call_ldconfig;
    }

    # Decide if we do it safely
    s/\bldconfig\b/BldconfigB/g;
    s/[ \t]//g;
    # this one matches code from debhelper
    s/^if\["\$1"=.?remove.?\];?\n*then\n*BldconfigB//gm;
    # variations...
    s/^if\[.?remove.?="\$1"\];?\n*then\n*BldconfigB//gm;
    s/^\["\$1"=.?remove.?\]\&&BldconfigB//gm;
    s/^\[.?remove.?="\$1"\]&&BldconfigB//gm;

	s!remove(?:|[^)]+)*\).*?BldconfigB.*?(;;|esac)!!s;

    if (/^[^\#]*BldconfigB/m) {
        tag "postrm-unsafe-ldconfig", "";
    }
}

}

# make /tmp/baz/baz.txt from /tmp/foo/../bar/../baz/baz.txt
sub abs_path {
    my $path = shift;
    while($path =~ s!/[^/]*/\.\./!/!g){1};
    return $path;
}

1;

# Local Variables:
# indent-tabs-mode: t
# cperl-indent-level: 4
# End:
# vim: syntax=perl sw=4 ts=8
