#!/usr/bin/perl
#
# $Id: prtcheckmissing,v 1.1 2003/10/27 15:26:50 opel Exp $
#      prtcheckmissing,v 1.2 2022/05/31 18:41:19 jmq Exp $
#      prtcheckmissing,v 1.3 2023/09/07 18:51:46 jmq Exp $

use strict;
use warnings;

sub check_missing {
  my @mask = parse_pkgadd_conf();
  open (my $dbh, "/var/lib/pkg/db") or die "Could not read package database!\n";
  local $/ = ""; # read files paragraph-wise; see ``perldoc perlvar''

  while(<$dbh>) {
    my ($name, $version, @files) = split /\n/;

    # apply the pkgadd rules to eliminate false positives
    my @missing = grep { (! -e "/$_") && wanted($_, @mask) } @files;

    # final report for this package
    next if not @missing;
    print map "/$_ $name\n", @missing;
  }
  close($dbh);
}

sub parse_pkgadd_conf {
  my @unwanted;
  local $/ = "\n";
  open my $pah, "< /etc/pkgadd.conf"
    or die "Couldn't open pkgadd install rules!\n";

  while(<$pah>) {
    next if /^\s*#/ or /^$/;
    my ($event, $pattern, $choice) = split;
    push (@unwanted, qr/$pattern/s) if ($event eq "INSTALL" and $choice eq "NO");
  }
  close ($pah);

  return @unwanted;
}

sub wanted {
  my $testfile = shift;
  my $retval = 1;
  while (my $regexp=shift) {
    $retval = 0 if $testfile =~ $regexp;
    last if $retval==0;
  }
  return $retval;
}

sub check_perms {
  my %wrongmode; # hash from suspicious file to the port(s) that claim to own it

  foreach (split('\n', `prt-get printf "%i:%p:%n\n"`)) {
    my ($isinst, $repo, $name) = split(':', $_, 3);
    next if ($isinst eq "no");
    open (my $fp,"$repo/$name/.footprint") or die "no footprint for $repo/$name";
    while(<$fp>) {
      my ($Emod,$Eown,$file) = split(/\t| -> /, $_, 3);
      chomp($file); $file =~ s/\/$//;
      next if (! -e "/$file"); # this case is handled by check_missing

      # there's a file on disk, so inspect its owner and perms
      my @stat = (-l "/$file") ? lstat("/$file") : stat("/$file");
      my $Fown = getpwuid($stat[4]) . "/" . getgrgid($stat[5]);
      my $Fmod = mode_to_string($stat[2]);
      $wrongmode{$file} .= " $name" if (($Eown ne $Fown) or ($Emod ne $Fmod));
    }
    close($fp);
  }

  # final report
  next if (not %wrongmode);
  print map "/$_ $wrongmode{$_}\n", sort keys %wrongmode; 
}

# inlined from Stat::lsMode in order to avoid an extra dependency
# (https://metacpan.org/pod/Stat::lsMode)
sub mode_to_string {
  my ($mode) = @_;
  my @perms = qw(--- --x -w- -wx r-- r-x rw- rwx);
  my @ftype = qw(. p c ? d ? b ? - ? l ? s D ? ?);
  $ftype[0] = '';
  my @str_mode = @perms[($mode&0700)>>6, ($mode&0070)>>3, $mode&0007];
  my $ftype = $ftype[($mode & 0170000)>>12];

  my $setids = ($mode & 07000)>>9;
  if ($setids) {
    if ($setids & 01) { # Sticky bit
      $str_mode[2] =~ s/([-x])$/$1 eq 'x' ? 't' : 'T'/e;
    }
    if ($setids & 02) { # Setgid bit
      $str_mode[1] =~ s/([-x])$/$1 eq 'x' ? 's' : 'S'/e;
    }
    if ($setids & 04) { # Setuid bit
      $str_mode[0] =~ s/([-x])$/$1 eq 'x' ? 's' : 'S'/e;
    }
  }
  return join '', $ftype, @str_mode;
}

if ($0 =~ /perms$/) { check_perms;
} else { check_missing; }
