#!/bin/bash
# test01_pkgquality -- autospec test (rpm quality checks)
# Copyright (C) 2008,2012-2014 Davide Madrisan <davide.madrisan@gmail.com>

[ -z "$BASH" ] || [ ${BASH_VERSION:0:1} -lt 2 ] &&
   echo $"this script requires bash version 2 or better" >&2 && exit 1

me=("test01_pkgquality" "1.45" "Thu Apr 23 2026")

[ -r /usr/share/autospec/lib/libmsgmng.lib ] ||
 { echo "$me: "$"library not found"": /usr/share/autospec/lib/libmsgmng.lib" 1>&2
   exit 1; }

. /usr/share/autospec/lib/libmsgmng.lib

[ -r /usr/share/autospec/lib/libtranslate.lib ] ||
 { echo "$me: "$"library not found"": /usr/share/autospec/lib/libtranslate.lib" 1>&2
   exit 1; }
. /usr/share/autospec/lib/libtranslate.lib

notify.debug $"loading"": \`test01_pkgquality'..."

# check if all the needed tools are available
for tool in file find getopt grep ls ldd wc; do
   [ "$(type -p $tool)" ] ||
      notify.error $"utility not found"": \`$tool'"
done

function alltests() {
   notify.note " ${NOTE}"$"performing quality checks""${NORM}""..."

   TEMP=`LC_ALL=C getopt \
      -o i:t: --long infofile:,tmpdir: \
      -n "$FUNCNAME" -- "$@"`
   [ $? = 0 ] || return 1
   eval set -- "$TEMP"

   while :; do
      case "$1" in
      -i|--infofile)
         rpminfofile="$2"
         shift
      ;;
      -t|--tmpdir)
         tmpextractdir="$2"
         shift
      ;;
      --) shift; break ;;
      *) notify.error $"\
(bug)"" -- $FUNCNAME: "$"\`getopt' error" ;;
      esac
      shift
   done

   [ "$rpminfofile" ] || notify.error $"\
(bug)"" -- $FUNCNAME: "$"missing mandatory arg"" (--infofile)"
   [ -r "$rpminfofile" ] || notify.error $"\
(bug)"" -- $FUNCNAME: "$"cannot read"" \`$rpminfofile'"

   . $rpminfofile

   [ "$tmpextractdir" ] || notify.error $"\
(bug)"" -- $FUNCNAME: "$"missing mandatory arg"" (--tmpdir)"
   [ -d "$tmpextractdir" ] || notify.error $"\
(bug)"" -- $FUNCNAME: "$"no such file or directory"" \`$tmpextractdir'"

   local total_issues=0

   # check for broken symlinks
   #  - symlinks to files in the buildroot directory for rpm
   #    (usable for a symlink attacks)
   #  - symlinks not pointing to existing files
   
   test.skip $test_number || {
   notify.note "$(test.num2str). ${NOTE}"\
$"checking for wrong symbolic links""${NORM}..."

   # local rpmbuildroot=`sed -n "/%description/q;{
   #    /^BuildRoot[ ]*:/{s/[^ ]*[ ]*//;p}}" \
   #       $spec_dir/$SRPM_SPECFILE | \
   #    sed "s,%[{]*name[}]*,$SPEC_NAME,
   #         s,%[{]*version[}]*,$SPEC_VERSION,
   #         s,%[{]*_tmppath[}]*,$tmppath_dir,;p"`

   # FIXME: 'tmppath_dir' should be get from configuration files
   tmppath_dir=`rpm --eval %_tmppath 2>/dev/null`

   [ "$tmppath_dir" ] ||
      notify.error $"(bug)"" -- $FUNCNAME: ""empty string"" (tmppath_dir)"

   notify.debug "tmppath_dir = $tmppath_dir"

   let "i = 0"
   for pck in ${rpmpkg_name[@]}; do
      pushd $tmpextractdir/$i >/dev/null
      for f in $(find -mindepth 1 -type l); do
         notify.debug "$f --> `readlink $f`"
         # FIXME: the check fails if 'BuildRoot' doesn't start
         #        by '%_{tmppath}'
         # note: the first condition check for wrong links, like
         #    /usr/share/man/man1/zcmp.1.gz -> .gz
         # made by the broken `brp-compress' script in rpm 4.0.4
         if [[ "$(readlink $f)" = ".gz" || \
               "$(readlink $f)" =~ $tmppath_dir ]]; then
            notify.warning "${NOTE}${pck##*/}${NORM}"
            notify.note $"\
wrong symlink"": \`${NOTE}${f/./}${NORM}' --> \`${NOTE}$(readlink $f)${NORM}'"
            let "total_issues += 1"
         fi
      done
      popd >/dev/null
      let "i += 1"
   done; }
   test_number=$(($test_number + 1))

   # check for `%buildroot' strings
   test.skip $test_number || {
   if [ "$rpm_ignores_buildroot" = 1 ]; then
      if [ "$SPEC_BUILDROOT" ]; then
         notify.note \
"$(test.num2str). ${NOTE}"\
$"checking for \`$SPEC_BUILDROOT' (%buildroot) strings"\
"${NORM}... "$"skipped"
      else
         notify.note \
"$(test.num2str). ${NOTE}"\
$"checking for %buildroot strings""${NORM}... "$"N/A"
      fi
   else
      notify.note "$(test.num2str). "\
$"checking for \`$SPEC_BUILDROOT' (%buildroot) strings"

      [ "$SPEC_BUILDROOT" ] || notify.error \
            $"(bug)"" -- $FUNCNAME: ""empty string"" (SPEC_BUILDROOT)"

      let "i = 0"
      for pck in ${rpmpkg_name[@]}; do
         pushd $tmpextractdir/$i >/dev/null
         for f in $(find -type f \
                     -exec grep -ls "$SPEC_BUILDROOT" {} \;2>/dev/null); do
            notify.warning "${NOTE}${pck##*/}${NORM}"
            notify.note "\
   ${NOTE}$(echo $f | sed "s,$tmpextractdir/$i,," )${NORM}"
            notify.note "\
    "$"mime type: ""${NOTE}$(file --brief --mime --uncompress $f)${NORM}"
            notify.note "$(\
strings -a $f | grep "^$SPEC_BUILDROOT" | sort -bu | \
sed "s,$SPEC_BUILDROOT\(.*\),    - [%buildroot]\1,")"
            let "total_issues += 1"
         done
         popd >/dev/null
         let "i += 1"
      done
   fi; }
   test_number=$(($test_number + 1))

   # check for `%_builddir' strings
   BUILDDIR="$(rpm --eval=%_builddir 2>/dev/null)"
   test.skip $test_number || {
   notify.note "$(test.num2str). ${NOTE}"\
$"checking for \`$BUILDDIR' (%_builddir) strings""${NORM}... "

   [ "$BUILDDIR" ] ||
      notify.error $"(bug)"" -- $FUNCNAME: ""empty string"" (BUILDDIR)"

   let "i = 0"
   for pck in ${rpmpkg_name[@]}; do
      pushd $tmpextractdir/$i >/dev/null
      for f in $(find -type f \
                  -exec grep -ls "$BUILDDIR" {} \; 2>/dev/null); do
         notify.warning "${NOTE}${pck##*/}${NORM}"
         notify.note "\
   ${NOTE}$(echo $f | sed "s,$tmpextractdir/$i,," )${NORM}"
         notify.note "\
    "$"mime type: ""${NOTE}$(file --brief --mime --uncompress $f)${NORM}"
         notify.note "$(\
strings -a $f | grep "$BUILDDIR" | sort -bu | \
sed "s,$BUILDDIR,[%_builddir],g;s,.*,    - &,")"
         let "total_issues += 1"
      done
      popd >/dev/null
      let "i += 1"
   done; }
   test_number=$(($test_number + 1))

   # check for suspected plugins (.la, .so) in devel packages
   # note: pure plugins must be in the main package, not in devel
   test.skip $test_number || {
   notify.note "$(test.num2str). ${NOTE}"\
$"checking for suspicious plugins in devel packages""${NORM}..."

   let "i = 0"
   for pck in ${rpmpkg_name[@]}; do
      # skip non devel packages
      [[ "${pck##*/}" =~ -devel- ]] || { let "i += 1"; continue; }

      pushd $tmpextractdir/$i >/dev/null
      # find *.so files that are not symlinks to dynamic libraries
      for f in `\
find -mindepth 1 -type f -name \*.so -exec file {} \; | \
grep ' shared object,' | sed -n 's/.\(.*\):.*/\1/p'`; do
         notify.warning "${NOTE}${pck##*/}${NORM}"
         notify.note $"found suspect plugin \`${NOTE}$f${NORM}'"
         let "total_issues += 1"
      done
      popd >/dev/null
      let "i += 1"
   done; }
   test_number=$(($test_number + 1))

   # check for wrong file attributes in lib and bin dirs
   test.skip $test_number || {
   notify.note "$(test.num2str). ${NOTE}"\
$"checking for wrong file attributes in bin and lib directories""${NORM}..."

   warning=0
   let "i = 0"
   for pck in ${rpmpkg_name[@]}; do
      pushd $tmpextractdir/$i >/dev/null
      for f in $( find . -type f \( \
         \( -name '*.so.*' -and -not -name '*.so.owner' \
               -and -not -perm 755 \) -or \
         \( -name '*.so' -and -not -perm 755 \) -or \
         \( \( -path './bin/*' -or \
               -path './sbin/*' -or \
               -path './usr/bin/*' -or \
               -path './usr/sbin/*' \) \
             -not -perm -111 \) 2>/dev/null \) ); do
         let "warning = 1" &&
         notify.warning "${NOTE}${pck##*/}${NORM}"
         notify.note $"found suspect file"": \
\`${NOTE}${f/./}${NORM}' [$(ls -l "$f" | sed 's, .*,,')]"
         let "total_issues += 1"
      done
      let "i += 1"
      popd >/dev/null
   done
   [[ $warning -eq 0 ]] || notify.note "\
-----------------------------
${NOTE}"$"Hint"":${NORM}
   # fixup strange shared library permissions
   chmod 755 %{buildroot}%{_libdir}/*.so*

   %files
   %defattr(-,root,root)
   ...
   %attr(0755,root,root) %{_bindir}/<program>
-----------------------------"; }
   test_number=$(($test_number + 1))

   # check for libraries with undefined symbols
   test.skip $test_number || {
   notify.note "$(test.num2str). ${NOTE}"\
$"checking for libraries with undefined symbols after relocation""${NORM}..."

   let "i = 0"
   local undefsyms
   for pck in ${rpmpkg_name[@]}; do
      # skip debug packages
      [[ "${pck##*/}" =~ -debug- ]] && { let "i += 1"; continue; }

      pushd $tmpextractdir/$i >/dev/null
      for f in `find -mindepth 1 -type f \
         \( -name *\.so\.* -not -name *.debug \) -exec file {} \; | \
         grep 'shared object' | sed -n 's/.\([^:]*\):.*/\1/p'`; do
         undefsyms="\
$(LC_ALL=C ldd -d -r "$f" 2>/dev/null |& grep "undefined symbol")"
         if [ "$undefsyms" ]; then
            notify.warning "${NOTE}${pck##*/}${NORM}"
            echo -n "${CRIT}"
            LC_ALL=C ldd -d -r "$f" 2>&1 | \
            grep "undefined symbol" | sed "s,.*,     - &,"
            echo -n "${NORM}"
            let "total_issues += 1"
         fi
      done
      popd >/dev/null
      let "i += 1"
   done; }
   test_number=$(($test_number + 1))

   # check for binary files in etc (see FHS-2.2)
   test.skip $test_number || {
   notify.note "$(test.num2str). ${NOTE}"\
$"checking for binary files installed in /etc (see FHS)""${NORM}..."

   warning=0
   let "i = 0"
   for pck in ${rpmpkg_name[@]}; do
      pushd $tmpextractdir/$i >/dev/null
      for f in $( find ./etc -type f -perm /111 2>/dev/null ); do
         case $f in
         ./etc/rc.d/init.d/*) ;;
         *) let "warning = 1" &&
             { notify.warning "${NOTE}${pck##*/}${NORM}"
               notify.note $"found suspect file"": \
\`${NOTE}${f/./}${NORM}' [$(ls -l "$f" | sed 's, .*,,')]"
               let "total_issues += 1"; } ;;
         esac
      done
      popd >/dev/null
      let "i += 1"
   done
   [ "$warning" = 0 ] || notify.note "\
-----------------------------
${NOTE}"$"Hint"":${NORM}
   %files
   %defattr(-,root,root)
   ...
   %attr(0644,root,root) %{_sysconfdir}/<...file>
-----------------------------"; } #|| exit 1
   test_number=$(($test_number + 1))

   # check for installation code needed by info pages
   test.skip $test_number || {
   notify.note "$(test.num2str). ${NOTE}"\
$"checking if the info catalog is updated when necessary""${NORM}..."

   error=0
   let "i = 0"
   for pck in ${rpmpkg_name[@]}; do
      [[ -e $pck ]] || notify.error $"package not found"": \`${pck##*/}'"

      [[ "$(rpm -p -ql $pck |
         # FIXME: this check only works for FHS-compliant distros
         grep "^$(rpm --eval %_infodir)")" ]] ||
          { let "i += 1"; continue; }   # no info pages found
      #notify.debug "$FUNCNAME: info page(s) found"

      [[ "$(rpm -p -q --requires $pck 2>/dev/null | sed -n '
/^[ \t]*\/sbin\/install-info/p')" ]] || let "error+=1"

      # note: we just check for at list two occurences of the command
      #       '/sbin/install-info' in the package scriptlets
      [[ "$(rpm -p -q --scripts $pck 2>/dev/null | sed -n '
/^[ \t]*\/sbin\/install-info/p' | wc -l)" -ge 2 ]] || let "error+=1"

      [ "$error" = "0" ] ||
       { notify.warning "${NOTE}${pck##*/}${NORM}"
         notify.note $"info pages should be installed/uninstalled""${NORM}
---------------------------------------
${NOTE}"$"Hint"":${NORM}
%package [<subpackage>]
...
$(if [[ "$rpm_macro_installinfo_binary" ]]; then 
     echo "\
Requires(post):$rpm_macro_installinfo_binary
Requires(preun):$rpm_macro_installinfo_binary"
  else
     echo "\
Requires(post):${path_installinfo:-/sbin/install-info}
Requires(preun):${path_installinfo:-/sbin/install-info}"
  fi)

%post [<subpackage>]
$([[ "$rpm_macro_installinfo" ]] &&
   echo "$rpm_macro_installinfo %{name}.info" ||
   echo "${path_installinfo:-/sbin/install-info} %{name}.info")
exit 0

%preun [<subpackage>]
$([[ "$rpm_macro_uninstallinfo" ]] &&
   echo "$rpm_macro_uninstallinfo %{name}.info" ||
   echo "${path_installinfo:-/sbin/install-info} --delete %{name}.info")
exit 0
---------------------------------------"
         let "total_issues += $error"; }
   done; }
   test_number=$(($test_number + 1))

   # check packages for wrong user and/or group ownerships
   test.skip $test_number || {
   notify.note "$(test.num2str). ${NOTE}"\
$"checking packages for wrong user and/or group ownerships""${NORM}..."

   error=0
   idun="$(id -un)" idgn="$(id -gn)"
   let "i = 0"
   for pck in ${rpmpkg_name[@]}; do
      [[ -e $pck ]] || notify.error $"\
package not found"": \`${pck##*/}'"
      ( LC_ALL=C rpm -p -qlv $pck | \
        while read line; do
           set -- $line
           # FIXME : find a better check, perhaps using a range
           # of uid reserved for users
           if [[ "$idun" = "$3" || "$idgn" = "$4" ]]; then
              notify.warning "${NOTE}${pck##*/}${NORM}"
              notify.note $"found suspect file"": \
\`${NOTE}$9${NORM}' [uid:\`${NOTE}$3${NORM}', gid:\`${NOTE}$4${NORM}']"
              let "total_issues += 1"
           fi
        done )
   done; }
   test_number=$(($test_number + 1))

   # check for desktop files installed in non standard applnk dir
   test.skip $test_number || {
   notify.note "$(test.num2str). ${NOTE}"\
$"checking packages for desktop files installed in the applnk dir""${NORM}..."

   warning=0
   rpmdatadir=$(rpm --eval %_datadir 2>/dev/null)
   let "i = 0"
   for pck in ${rpmpkg_name[@]}; do
      pushd $tmpextractdir/$i >/dev/null
      for f in $( find .${rpmdatadir} -type f 2>/dev/null ); do
         case $f in
         .${rpmdatadir}/applnk/*.desktop)
            let "warning = 1" &&
             { notify.warning "${NOTE}${pck##*/}${NORM}"
               notify.note $"found suspect file"": \
\`${NOTE}${f/./}${NORM}' [$(ls -l "$f" | sed 's, .*,,')]"
               let "total_issues += 1"; } ;;
         *) ;;
         esac
      done
      popd >/dev/null
   done
   [ "$warning" = 0 ] || notify.note "\
-----------------------------
${NOTE}"$"Hint"":${NORM}
   "$"create desktop files for:"" ${rpmdatadir}/applications
   "$"see:"" <http://www.freedesktop.org/>
-----------------------------"; }
   test_number=$(($test_number + 1))

   # check if a package that do not contains binaries is tagged noarch
   test.skip $test_number || {
   notify.note "$(test.num2str). ${NOTE}"\
$"checking for packages with bad BuildArch tag""${NORM}..."

   warning=0
   let "i = 0"
   for pck in ${rpmpkg_name[@]}; do
      pushd $tmpextractdir/$i >/dev/null
      for f in $(LC_ALL=C find -mindepth 2 -type f \
         -exec file {} \; 2>/dev/null | grep -E "(\
 ELF | OCaml library | ar archive |\
 dynamically linked | statically linked )"); do
         notify.debug "found binary or library file: \`${NOTE}${f/./}${NORM}'"
         let "warning = 1"
         break
      done
      popd >/dev/null
   done
   if [ "$warning" = 0 ]; then
      [ "$SPEC_BUILDARCH" = "noarch" ] || 
       { notify.warning "${NOTE}${pck##*/}${NORM}"
         notify.note $"this package should be tagged \`noarch'""
-----------------------------
${NOTE}"$"Hint"":${NORM}
BuildArch:     noarch
-----------------------------"
         let "total_issues += 1"; }
   fi; }
   test_number=$(($test_number + 1))

   notify.note "
 --> "$"${NOTE}Quality checks: ${#rpmpkg_name[@]} \
package(s) checked: ${NORM}${WARN}$total_issues${NORM}${NOTE} warning(s).${NORM}""
"
}
