#! /usr/bin/perl

# check dynamic library dependecies

use strict;
use warnings;
use File::Find;
use Getopt::Long;

use Data::Dumper;
$Data::Dumper::Sortkeys = 1;
$Data::Dumper::Terse = 1;
$Data::Dumper::Indent = 1;

sub find_elf_files;
sub resolve_links;
sub is_elf;

my $opt_verbose = 0;

GetOptions(
  'verbose|v'        => sub { $opt_verbose++ },
);

# hash of all ELF objects with file name as key
#
# sample entry:
#
#  'usr/lib64/libcrypt.so.1.1.0' => {
#    'build_id' => '205ebdb358b973faa8f1dfed1202614f6f76e50a',
#    'needed' => [
#      'libc.so.6'
#    ],
#    'sonames' => [
#      'libcrypt.so.1',
#      'libcrypt.so.1.1.0'
#      'libowcrypt.so.1',
#    ]
#  },
#
my $elf_files = {};

# hash of all SONAME records with SONAME as key and file name as value
#
# sample entry
#
#  'libowcrypt.so.1' => 'usr/lib64/libcrypt.so.1.1.0',
#  'libp11-kit.so.0' => 'usr/lib64/libp11-kit.so.0.1.0',
#  'libpam.so.0' => 'lib64/libpam.so.0.84.2',
#
my $sonames;

# hash with all NEEDED records with NEEDED as key and array of file names as value
#
# sample entry
#
#  'libncursesw.so.6' => [
#    'usr/bin/pinentry-curses',
#    'usr/bin/watch',
#    'usr/lib64/libformw.so.6.1',
#    'usr/lib64/libmenuw.so.6.1',
#    'usr/lib64/libncurses++w.so.6.1',
#    'usr/lib64/libpanelw.so.6.1',
#    'usr/sbin/cfdisk',
#    'usr/sbin/cgdisk',
#    'usr/sbin/powertop'
#  ],
#
my $needed;

# hash of ld.so.cache entries with SONAME as key and file name as value
#
# sample entry
#
# 'libSegFault.so' => 'lib64/libSegFault.so',
#
my $ldconfig;

# set to 1 if a config error is detected
my $error = 0;

for my $dir (@ARGV) {
  die "usage: check_libs dir\n" unless -d $dir;

  print "analyzing ELF objects in $dir...\n";

  find_elf_files($dir, $elf_files);

  # write build ids
  if(open my $f, ">$dir.debugids") {
    for my $file (sort keys %$elf_files) {
      print $f "$elf_files->{$file}{build_id} $file\n" if $elf_files->{$file}{build_id}
    }
    close $f;
  }

  if( -f "$dir/etc/ld.so.cache") {
    for (`ldconfig -C $dir/etc/ld.so.cache -p`) {
      $ldconfig->{$1} = $2 if /^\s+(\S+)\s*.*=>\s*\/(\S+)/;
    }
  }
}

# print Dumper($elf_files);

for my $file (sort keys %$elf_files) {
  for my $s (@{$elf_files->{$file}{sonames}}) {
    $sonames->{$s} = $file;
  }

  for my $n (@{$elf_files->{$file}{needed}}) {
    push @{$needed->{$n}}, $file;
  }
}

exit $error if $error;

print "checking ld.so.cache...\n";
if($ldconfig) {
  my $first = 1;
  for (keys %$sonames) {
    # next if $sonames->{$_} =~ m#^usr/lib/YaST2/plugin#;
    next unless exists $needed->{$_};

    if(!exists $ldconfig->{$_}) {
      print "libs not in ld.so.cache:\n" if $first;
      printf "  %-32s  %s\n", $_, $sonames->{$_};
      printf "  [needed by: %s]\n", join(', ', @{$needed->{$_}}) if $opt_verbose >= 1;
      $first = 0;
    }
  }
  print "ok\n" if $first;
}
else {
  print "no ld.so.cache found\n";
}

print "checking for missing libs...\n";
my $first = 1;
for (sort keys %$needed) {
  if(!exists($sonames->{$_})) {
    printf "  %-32s  %s\n", $_, join(', ', @{$needed->{$_}});
    $first = 0;
    $error = 1;
  }
}
print "ok\n" if $first;

exit $error;


# Scan dir for ELF files, analyze them, and add the result to list.
#
# find_elf_files(dir, list)
#
# dir: directory to scan
# list: hash ref to add results to
#
# Note: all file names are relative to dir.
#
sub find_elf_files
{
  my $dir = $_[0];
  my $elf_files = $_[1];
  my $files;

  File::Find::find({
    wanted => sub {
      # skip kernel module directory
      return if m#/modules/#;
      my $aliases;
      ( $_, $aliases ) = resolve_links($dir, $_) if -l;
      my $short_name = $_;
      $short_name =~ s#^$dir/##;
      # if the file has been analyzed before, just add new symlinks as sonames
      my $elf = $elf_files->{$short_name} ? $elf_files->{$short_name} : is_elf($_);
      if($elf) {
        for my $alias (@$aliases, $_) {
          $alias =~ s#^$dir/##;
          my $a = $alias;
          $a =~ s#.*/##;
          push @{$elf->{sonames}}, $a if $a ne "";
        }
        my $so;
        $so->{$_} = 1 for (@{$elf->{sonames}});
        $elf->{sonames} = [ sort keys %$so ];
        $elf_files->{$short_name} = $elf;
      }
    },
    no_chdir => 1
  }, $dir);
}


# Resolve symlinks.
#
# (file, aliases) = resolve_links(dir, link)
#
# Resolve link recursively, assuming dir as base directory.
#
# Returns the final file name and a list of all symlinks encountered as aliases.
#
sub resolve_links
{
  my $dir = $_[0];
  my $file = $_[1];
  my $aliases;

  while(-l $file) {
    my $link = readlink $file;
    $aliases->{$file} = $link;
    if($link =~ m#^/#) {
      $file = "$dir$link";
    }
    else {
      $file =~ s#[^/]*$#$link#;
    }
    if($aliases->{$file}) {
      print "symlink loop detected: $file -> $aliases->{$file}\n";
      $error = 1;
      last;
    }
  }

  return ( $file, [ sort keys %$aliases ] );
}


# Analyze ELF file.
#
# result = is_elf(file)
#
# Return undef if file is not an ELF file. Otherwise some ELF header data are returned.
#
# See decscription of global variable $elf_files for a sample result record.
#
sub is_elf
{
  my $file = $_[0];

  if(-f($file) && open(my $f, "<", $file)) {
    my $buf;
    sysread $f, $buf, 4;
    close $f;

    return undef if $buf ne "\x7fELF";
  }
  else {
    return undef;
  }

  my $elf;

  for (`readelf -d -n $file 2>/dev/null`) {
    push @{$elf->{needed}}, $1 if /\s\(NEEDED\)\s*Shared\slibrary:\s*\[(\S+)\]/;
    push @{$elf->{sonames}}, $1 if /\s\(SONAME\)\s*Library\ssoname:\s*\[(\S+)\]/;
    $elf->{build_id} = $1 if /\sBuild ID:\s*([0-9a-z]*)/;
  }

  return $elf
}
