package SBO::Lib::Solibs;

# vim: ts=2:et

use 5.016;
use strict;
use warnings;

our $VERSION = '4.4.2';

use SBO::Lib::Util qw/ :config :const in uniq error_code script_error slurp /;
use SBO::Lib::Pkgs qw/ $perl_pkg $ruby_pkg /;

use Exporter 'import';
use File::Basename;

use sigtrap qw/ handler _caught_signal ABRT INT QUIT TERM /;

our @EXPORT_OK = qw{
  decimalize
  elf_links
  installed_solibs
  series_check
  solib_check
  update_known_solibs

  $match_mode
  @native_libs
  %old_libs
  @libs_32
  @not_opt_only
};

our %EXPORT_TAGS = (
  all => \@EXPORT_OK,
);

=pod

=encoding UTF-8

=head1 NAME

SBO::Lib::Solibs - Routines for evaluating ELF binaries and checking compatibility.

=head1 SYNOPSIS

  use SBO::Lib::Solibs qw/ solib_check /;

=head1 VARIABLES

=head2 $match_mode

A variable indicating that the user is interested in files and packages depending on one or
more libraries. It is set when the C<--lib-search> flag is in use for C<sbocheck(1)>.

=head2 @native_libs

An array with shared objects (solibs) of the native architecture in the C<ldconfig(1)> cache.
It is generated by C<solib_check()> via C<update_known_solibs()> if it is empty at the time
of calling.

=head2 %old_libs

A hash with a per-package list of apparently missing first-order shared object dependencies;
each missing dependency comes with a list of files that link to it. This hash is generated
when running C<solib_check()>.

=head2 %per_cand, %x86_per_cand

Hashes with per-object lists of dynamically-linked files. They are used to produce the file lists
in C<%old_libs>, and are not exported.

=head2 @libs_32

An array with 32-bit shared objects in the C<ldconfig(1)> cache. Used only under 64-bit
architectures, it is generated together with C<@native_libs> by C<update_known_solibs()>.

=head2 @not_opt_only

An array with packages with files outside of C</opt> that have missing solib dependencies.

=cut

# ELF-related variables
my $elf_bytes = "7f454c46";

my $word = 4;
my $addr_64 = 8;
my $addr_32 = 4;
my $xword_64 = 8;
my $xword_32 = 4;
my $off_64 = 8;
my $off_32 = 4;
my $half = 2;
my $pre = 16;

my $word_H = $word*2;
my $addr_64_H = $addr_64*2;
my $addr_32_H = $addr_32*2;
my $xword_64_H = $xword_64*2;
my $xword_32_H = $xword_32*2;
my $off_64_H = $off_64*2;
my $off_32_H = $off_32*2;
my $half_H = $half*2;

my $dynamic_type_little = "06000000";
my $dynamic_type_big = "00000006";
my $needed_type_little = "01000000";
my $needed_type_big = "00000001";
my $runpath_type_little = "1d000000";
my $runpath_type_big = "0000001d";
my $rpath_type_little = "0f000000";
my $rpath_type_big = "0000000f";

# For checking objects.
our @native_libs;
our @libs_32;
our %old_libs;
our %per_cand;
our %x86_per_cand;
our @not_opt_only;

our $match_mode;

my $is_64 = $arch =~ /64/;
my $perl_arch = $arch;
$perl_arch = ($perl_arch =~ /86$/) ? "x86" : "arm" unless $is_64;

my %ran_solibs;
my @check_perl;

# determine relevant perl binary origin times
my ($inst_perl_pkg_time, $inst_perl_bin_time, $perl_major_bin_time, $perl_major_pkg_time, $perl_major);
my (%removed_perls, @removed_perls);

my (@py_installed, @py_missing);
my $rubyver;

=head1 SUBROUTINES

=head2 decimalize

  my $decimal = decimalize($hex, $big_endian);

C<decimalize()> takes a hex string and an indicator of whether big-endian processing is to be
used, returning a decimal string.

=cut

sub decimalize {
  script_error("decimalize requires two arguments.") unless @_ == 2;
  my ($string, $big_endian) = @_;
  script_error("decimalize requires a string with an even number of characters.") unless length($string) % 2 == 0;
  my $new_string;
  unless ($big_endian) {
    while ($string) {
      $new_string .= substr $string, -2;
      $string = substr $string, 0, length($string)-2;
    }
  } else {
    $new_string = $string;
  }
  return hex $new_string;
}

=head2 elf_links

  my ($elf_type, @cand_libs) = is_elf($file);

C<elf_links()> takes a path and checks whether it is a dynamically-linked ELF binary; it
returns 0 if not. Otherwise, it reads entries according to the elf(5) specification and
returns 1 for a 64-bit ELF file, -1 for a 32-bit ELF file and an array of first-order
shared object dependencies that do not exist on the system under an C<rpath> or C<runpath>.

=cut

sub elf_links {
  script_error("elf_links requires an argument; exiting.") unless @_ == 1;
  my $file = shift;
  open my $fh, "<:raw", $file or return 0;
  my ($read_in, $contents);
  $read_in = read $fh, $contents, $pre;
  unless (defined $read_in) { close $fh; undef $fh; return 0; }
  unless ($read_in == $pre) { close $fh; undef $fh; return 0; }
  # need to be hardcoded until the architecture is determined
  my ($elf, $elf_type, $end) = unpack "H8 H2 H2", $contents;
  unless ($elf eq $elf_bytes) { close $fh; undef $fh; return 0; }
  my ($is_32, $big_endian);
  $big_endian = $end eq "02" ? 1 : 0;
  $is_32 = $elf_type eq "01" ? 1 : 0;

  # get usable variables based on file characteristics
  my ($addr, $addr_H, $off, $off_H, $xword, $xword_H);
  my ($dynamic_type, $needed_type, $runpath_type, $rpath_type, $padding);
  if ($is_32) {
    $padding = '';
    $addr = $addr_32;
    $addr_H = $addr_32_H;
    $off = $off_32;
    $off_H = $off_32_H;
    $xword = $xword_32;
    $xword_H = $xword_32_H;
  } else {
    $padding = "00000000";
    $addr = $addr_64;
    $addr_H = $addr_64_H;
    $off = $off_64;
    $off_H = $off_64_H;
    $xword = $xword_64;
    $xword_H = $xword_64_H;
  }
  if ($big_endian) {
    $dynamic_type = $dynamic_type_big;
    $needed_type = $padding. $needed_type_big;
    $runpath_type = $padding. $runpath_type_big;
    $rpath_type = $padding. $rpath_type_big;
  } else {
    $dynamic_type = $dynamic_type_little;
    $needed_type = $needed_type_little. $padding;
    $runpath_type = $runpath_type_little. $padding;
    $rpath_type = $rpath_type_little. $padding;
  }

  # information about the section header table
  seek $fh, $pre+$half*2+$word+$xword*2, 0;
  my $sh_data = $xword+$word+$half*6;
  my ($sec_head_info, $sh_contents);
  $sec_head_info = read $fh, $sh_contents, $sh_data;
  unless ($sec_head_info == $sh_data) { close $fh; undef $fh; return $0; }
  my ($sh_offset, $sh_entry, $sh_num) = unpack "H$xword_H x$word x$half x$half x$half H$half_H H$half_H", $sh_contents;
  $sh_offset = decimalize($sh_offset, $big_endian);
  $sh_entry = decimalize($sh_entry, $big_endian);
  $sh_num = decimalize($sh_num, $big_endian);

  # finding the dynamic section entry; one and only one is
  # mandatory for dynamically-linked ELF
  seek $fh, $sh_offset, 0;
  my ($dyn_link, $dynamic_location, $dynamic_size, $dynamic_entry);
  my $i = 0;
  while ($i < $sh_num) {
    $i++;
    my $section_contents;
    my $section_info = read $fh, $section_contents, $sh_entry;
    unless ($section_info == $sh_entry) { close $fh; undef $fh; return 0; }
    my ($type, $offset, $size, $link, $entry) = unpack "x$word H$word_H x$xword x$xword H$xword_H H$xword_H H$word_H x$word x$xword H$xword_H", $section_contents;
    if ($type eq $dynamic_type) {
      $dyn_link = decimalize($link, $big_endian);
      $dynamic_location = decimalize($offset, $big_endian);
      $dynamic_size = decimalize($size, $big_endian);
      $dynamic_entry = decimalize($entry, $big_endian);
      last;
    }
  }
  unless (defined $dyn_link) { close $fh, undef $fh, return 0; }

  # find the section header entry with the appropriate string table
  seek $fh, $sh_offset+$dyn_link*$sh_entry, 0;
  my $str_entry_data = $word*2+$xword+$addr+$off;
  my ($string_info, $string_contents);
  $string_info = read $fh, $string_contents, $str_entry_data;
  unless ($string_info == $str_entry_data) { close $fh; undef $fh; return 0; }
  my $str_offset = unpack "x$word x$word x$xword x$addr H$xword_H", $string_contents;
  $str_offset = decimalize($str_offset, $big_endian);

  # read the dynamic section for the proper string table offsets for
  # rpaths and objects
  unless ($dynamic_size % $dynamic_entry == 0) { close $fh; undef $fh; return 0; }
  my $dynamic_entries = $dynamic_size / $dynamic_entry;
  seek $fh, $dynamic_location, 0;
  my (@needed, @rpaths);
  $i = 0;
  while ($i < $dynamic_entries) {
    $i++;
    my ($dyn_entry, $dyn_contents);
    $dyn_entry = read $fh, $dyn_contents, $dynamic_entry;
    unless ($dyn_entry == $dynamic_entry) { close $fh; undef $fh; return 0; }
    my ($type, $offset) = unpack "H$xword_H H$xword_H", $dyn_contents;
    if ($type eq $needed_type) {
      push @needed, decimalize($offset, $big_endian) + $str_offset;
    } elsif ($type eq $runpath_type or $type eq $rpath_type) {
      push @rpaths, decimalize($offset, $big_endian) + $str_offset;
    }
  }

  # read the strings
  my $dirname = dirname $file if @rpaths;
  my (@cand_libs, @cand_rpaths);
  CANDS: for my $cand (@rpaths, @needed) {
    seek $fh, $cand, 0;
    my $string;
    my $bits = 1;
    while ($bits) {
      my $byte = read $fh, my $byte_contents, 1;
      unless ($byte == 1) { close $fh; undef $fh; return 0; }
      $bits = unpack "H2", $byte_contents;
      last if $bits eq "00";
      my $char = unpack "A1", $byte_contents;
      $string .= $char;
    }
    if (in $cand, @rpaths) {
      $string =~ s/\$ORIGIN/$dirname/g;
      push @cand_rpaths, split ":", $string;
    } else {
      next CANDS unless $string =~ m/\.so(|\.\d+(|\.\d+(|\.\d+)))$/;
      unless ($match_mode) {
        for my $rpath (@cand_rpaths) {
          next CANDS if -f "$rpath/$string" or -l "$rpath/$string";
        }
      }
      if ($is_64 and $is_32) {
        $x86_per_cand{$string} .= " $file";
      } else {
        $per_cand{$string} .= " $file";
      }
      push @cand_libs, $string;
    }
  }
  my $elf_return_value = $is_32 ? -1 : 1;
  close $fh;
  undef $fh;
  if (in "libperl.so", @cand_libs or in "/usr/lib64/perl5/CORE", @cand_rpaths or in "/usr/lib/perl5/CORE", @cand_rpaths) { push @check_perl, $file; }
  return $elf_return_value, @cand_libs;
}

=head2 initialize_perl

  initialize_perl();

C<initialize_perl()> determines the installed major C<perl> version, installation
times for installed and removed C<perl> packages and the date at which the installed
major version was first built for the running Slackware architecture (if available).
These values are used to perform the C<perl> package test. There is no useful return value.

=cut

sub initialize_perl {
  undef $inst_perl_bin_time;
  undef $perl_major_bin_time;
  undef $perl_major_pkg_time;
  splice @removed_perls;
  %removed_perls = ();
  $inst_perl_pkg_time = (stat "$pkg_db/$perl_pkg")[9];
  my $perl_bin = "/usr/bin/" . readlink "/usr/bin/perl";
  $inst_perl_bin_time = (stat($perl_bin))[9];
  $perl_major = $perl_bin;
  $perl_major =~ s!/usr/bin/perl!!;
  $perl_major =~ s/\.\d+$//;
  my @perl_vers = split "\n", slurp "$conf_dir/perl_vers";
  for (@perl_vers) {
    ($perl_major_bin_time) = $_ =~ /^(\d+)\t$perl_major\t$perl_arch$/;
    last if defined $perl_major_bin_time;
  }
  for (glob "$rem_pkg_db/perl-5*") {
    my $rem_timestamp = (stat($_))[9];
    push @removed_perls, $rem_timestamp;
    $removed_perls{$rem_timestamp} = $_;
  }
}

=head2 initialize_python

  initialize_python();

C<initialize_python()> determines the default installed C<python2> and C<python3>
versions in the form e.g. C<python3.12>. There is no useful return value.

=cut

sub initialize_python {
  splice @py_installed;
  splice @py_missing;
  my $py3ver = `python3 --version` if -x "/usr/bin/python3";
  if (defined $py3ver) {
    $py3ver =~ s/(\s|\.\d+$)//g;
    $py3ver = lc $py3ver;
    push @py_installed, $py3ver;
  }
  my $py2ver = `python2 --version 2>&1` if -x "/usr/bin/python2";
  if (defined $py2ver) {
    $py2ver =~ s/(\s|\.\d+$)//g;
    $py2ver = lc $py2ver;
    push @py_installed, $py2ver;
  }
}

=head2 initialize_ruby

  initialize_ruby();

C<initialize_ruby()> determines the correct major version for C<ruby> based
on the C</usr/include/ruby-*> directory. There is no useful return value.

=cut

sub initialize_ruby {
  if ($ruby_pkg) {
    my $fh;
    # non-fatal
    if (open $fh, "<", "$pkg_db/$ruby_pkg") {
      for (<$fh>) {
        next unless $_ =~ /^usr\/include\/ruby-/;
        ($rubyver) = $_ =~ m/^usr\/include\/ruby-(\d+\.\d+\.\d+)\/$/;
        last;
      }
      close $fh;
    }
  }
}

=head2 installed_solibs

  my @pkg_solibs = installed_solibs($pkg);

C<installed_solibs()> takes the name of a package file. It returns an array of
shared object names that are installed as files or symlinks, or 0 if none exist.

=cut

sub installed_solibs {
  script_error("installed_solibs requires an argument.") unless @_ == 1;
  my $pkg = shift;
  unless (-f "$pkg_db/$pkg") { return 0; }
  my $exit = open(my $fh, "<", "$pkg_db/$pkg") == 0;
  if ($exit) { return 0; }
  my ($start_reading, @pkg_solibs);
  for my $line (<$fh>) {
    $start_reading = 1 if $line eq "./\n";
    next unless defined $start_reading;
    chomp($line);
    next unless $line =~ m/\.so(|\.\d+(|\.\d+(|\.\d+)))$/;
    push @pkg_solibs, basename $line;
  }
  close $fh;
  undef $fh;
  unless (-f "$script_db/$pkg") { return @pkg_solibs; }
  my $script_exit = open(my $sfh, "<", "$script_db/$pkg") == 0;
  if ($script_exit) { return @pkg_solibs; }
  for my $line (<$sfh>) {
    chomp($line);
    next unless $line =~ m/^\( cd .* ; rm -rf/;
    my $cand = (split " ", $line)[-2];
    next unless $cand =~ m/\.so(|\.\d+(|\.\d+(|\.\d+)))$/;
    push @pkg_solibs, basename $cand;
  }
  close $sfh;
  undef $sfh;
  return @pkg_solibs;
}

=head2 series_check

  my @series_good = series_check($pkg, @series);

C<series_check()> takes the name of a package file and an array with one or more checks
to perform. Available checks include C<perl>, C<python> and C<ruby> at this time.

Likely stock packages (i.e., untagged packages) are judged to be incompatible with system
C<perl> if they are older than the first C</usr/bin/perl*> binary of the installed major
version added to Slackware. The relevant timestamps are stored in C</etc/sbotools/perl_vers>.
Other packages are compared with the earliest installation date of the installed major version,
or the binary timestamp if unavailable.

C<python> and C<ruby> are judged to be incompatible if files associated with the wrong
major version (e.g. C<python-3.12> or C<ruby-3.4*>) are included.

Files in C</opt> are ignored.

The subroutine returns an array with results for the checks in alphabetical order, with
1 indicating apparent compatibility and 0 indicating apparent incompatibility.

=cut

sub series_check {
  script_error("series_check requires at least two arguments.") unless @_ >= 2;
  my ($pkg, @series) = @_;
  update_known_solibs() unless @native_libs;
  my $perl_check = in "perl", @series;
  my $python_check = in "python", @series;
  my $ruby_check = in "ruby", @series;
  my $good_perl = 1;
  my $good_python = 1;
  my $good_ruby = 1;
  my $stock_pkg = $pkg =~ /(-\d+|_slack\d+\.\d+)$/;
  my $exit = open(my $fh, "<", "$pkg_db/$pkg") == 0;
  error_code("Opening $pkg_db/$pkg failed.", _ERR_OPENFH) if $exit;
  if ($perl_check and not exists $ran_solibs{$pkg}) {
    solib_check($pkg);
  }
  my ($start_reading, @file_list);
  for my $line (<$fh>) {
    $start_reading = 1 if $line eq "./\n";
    next unless defined $start_reading;
    chomp($line);

    if ($python_check) {
      if ($good_python and $line =~ /\/python\d+\.\d+\/site-packages\//) {
        next if $line =~ /^opt\//;
        my ($sought_python) = $line =~ m/\/(python\d+\.\d+)\/site-packages\//;
        next if in $sought_python, @py_installed;
        if (in $sought_python, @py_missing) {
          $good_python = 0;
          next;
        }
        if (-x "/usr/bin/$sought_python") {
          push @py_installed, $sought_python;
          next;
        } else {
          push @py_missing, $sought_python;
        }
        $good_python = 0;
      }
    }

    if ($ruby_check) {
      if ($good_ruby and $line =~ /\/ruby\/gems\/\w/) {
        unless (defined $rubyver) {
          $good_ruby = 0;
          next;
        }
        $good_ruby = 0 unless $line =~ /\/ruby\/gems\/$rubyver\// or $line =~ /^opt\//;
      }
    }

    if ($perl_check) {
      next unless $good_perl;
      unless (in "/$line", @check_perl) {
        next unless $line =~ /\/lib(|64)\/perl/;
        next unless $line =~ /\.so$/;
        next if $line =~ /^opt\//;
        next unless -x -B -f "/$line";
      }
      my $lib_time = (stat("/$line"))[9];
      unless ($stock_pkg) {
        unless ($inst_perl_pkg_time < $lib_time) {
          my @older_perls;
          for (@removed_perls) {
            push @older_perls, $_ if $_ < $lib_time;
          }
          unless (@older_perls) {
            $good_perl = 0;
            next;
          }
          @older_perls = sort @older_perls;
          my $built_with = $removed_perls{$older_perls[-1]};
          ($built_with) = $built_with =~ /perl-(\d+\.\d+)\.\d+/;
          $good_perl = 0 unless defined $built_with;
          $good_perl = 0 unless $built_with eq $perl_major;
        }
      } else {
        next if $inst_perl_bin_time <= $lib_time;
        $good_perl = 0 unless defined $perl_major_bin_time;
        $good_perl = 0 unless $perl_major_bin_time <= $lib_time;
      }
    }
  }

  return ($good_perl, $good_python, $good_ruby);
}

=head2 solib_check

  my $solibs_good = solib_check($pkg);

  my $solibs_good = solib_check($pkg, @search);

C<solib_check()> takes the name of a package file and, optionally, an array of shared
object names to search, and checks for the presence of any required first-order shared
object dependencies as based on the C<@native_libs> shared object array. It returns 1 if
all required shared objects appear to be present and 0 otherwise.

If C<$match_mode> is set, C<solib_check()> instead returns 0 if any file in the package
has a first-order dependency on a library named by the user, present or not.

Because C<elf_links()> is called, performance is cache-dependent. It is best to call
C<solib_check()> judiciously.

=cut

sub solib_check {
  script_error("solib_check requires at least one argument.") unless @_ >= 1;
  my ($pkg, @search) = @_;
  update_known_solibs() unless @native_libs;
  $ran_solibs{$pkg} = 1;
  my $exit = open(my $fh, "<", "$pkg_db/$pkg") == 0;
  error_code("Opening $pkg_db/$pkg failed.", _ERR_OPENFH) if $exit;
  my ($start_reading, $not_opt_only, @file_list);
  for my $line (<$fh>) {
    $start_reading = 1 if $line eq "./\n";
    next unless defined $start_reading;
    chomp($line);
    next unless -f -x "/$line" or $line =~ m/\.so(|\.\d+(|\.\d+(|\.\d+)))$/;
    push @file_list, "/$line";
  }
  close $fh;
  undef $fh;
  my (@nonexistent, @shared, @x86_shared);
  for my $file (@file_list) {
    next unless my ($elf_links, @cands) = elf_links($file);
    if ($is_64) {
      push @shared, @cands if $elf_links gt 0;
      push @x86_shared, @cands if $elf_links lt 0;
    } else {
      push @shared, @cands;
    }
  }
  return 1 unless @shared or @x86_shared;
  for my $cand (uniq sort @shared) {
    if (@search) { next unless in $cand, @search; }
    next if not $match_mode and in $cand, @native_libs;
    unless (not $match_mode and solib_present($cand, $pkg, @file_list)) {
      push @nonexistent, "  $cand:";
      for my $file (uniq sort split " ", $per_cand{$cand}) {
        push @nonexistent, "    $file" if in $file, @file_list;
        $not_opt_only = 1 unless $file =~ m!^/opt/!;
      }
    }
  }
  for my $cand (uniq sort @x86_shared) {
    if (@search) { next unless in $cand, @search; }
    next if not $match_mode and in $cand, @libs_32;
    unless (not $match_mode and solib_present($cand, $pkg, @file_list)) {
      push @nonexistent, "  $cand (x86):";
      for my $file (uniq sort split " ", $x86_per_cand{$cand}) {
        push @nonexistent, "    $file" if in $file, @file_list;
        $not_opt_only = 1 unless $file =~ m!^/opt/!;
      }
    }
  }
  undef @file_list;
  undef @shared;
  undef @x86_shared;
  if (@nonexistent) {
    push @nonexistent, "";
    $old_libs{$pkg} = join("\n", @nonexistent);
    push @not_opt_only, $pkg if $not_opt_only;
    return 0;
  }
  return 1;
}

=head2 solib_present

  my $solib_present = solib_present($cand_lib, $pkg, @candidate_files);

C<solib_present()> takes the name of the shared object to be checked, the name of
a package file and an array with probable ELF files shipped by that package. It returns 1 if
the shared object appears to be present and 0 if it does not.

Please note that the known shared object array C<@native_libs> (and C<@libs_32> if running
on a 64-bit architecture) is the main source of shared object verification.
C<solib_present()> is called after this first verification step fails. Shared objects
that are neither shipped nor created as symlinks by the package can be missed.

This subroutine is not exported.

=cut

sub solib_present {
  script_error("solib_present requires more than two arguments.") unless @_ > 2;
  my ($cand_lib, $pkg, @file_list) = @_;
  return 1 if $cand_lib =~ m/^\// and -f $cand_lib;
  my $grep_lib = $cand_lib;
  $grep_lib =~ s/\+/\\+/;
  return 1 if grep { /\/$cand_lib$/ } @file_list;
  my $found_in_script;
  if (-f "$script_db/$pkg") {
    if (open(my $fh, "<", "$script_db/$pkg")) {
      for my $line (<$fh>) {
        next unless $line =~ m/^\( cd .* ; rm -rf $grep_lib \)$/;
        my @link_string = split(" ", $line);
        next unless -l "/$link_string[2]/$link_string[6]";
        $found_in_script = 1;
        last;
      }
      close $fh;
      undef $fh;
    }
    return 1 if defined $found_in_script;
  }
  return 0;
}

=head2 update_known_solibs

  update_known_solibs;

C<update_known_solibs()> takes no arguments. It uses the C<--print-cache> option of
C<ldconfig(1)> to generate an array of existent known shared objects, C<@native_libs>. On
64-bit systems, it generates C<@libs_32> as well, an array with 32-bit shared objects.

The C<initialize_*()> subroutines for the additional package tests are called at this time.

The script exits in case of C<ldcdonfig(1)> failure. There is no useful return value.

=cut

sub update_known_solibs {
  undef @native_libs;
  undef @libs_32;
  %ran_solibs = ();
  splice @py_installed;
  splice @py_missing;
  splice @not_opt_only;
  initialize_perl();
  initialize_python();
  initialize_ruby();
  %old_libs = ();
  my @ld_lines = split "\n", `/sbin/ldconfig --print-cache`;
  script_error("Getting the ldconfig cache failed. Exiting.") unless @ld_lines;
  for my $line (@ld_lines) {
    next unless $line =~ m/^\s/;
    my @item = split " ", $line;
    next unless -f $item[-1] or -l $item[-1];
    unless ($is_64) {
      push @native_libs, $item[0];
    } else {
      if ($line =~ m/\(libc6,[\S].+64(\)|, )/) {
        push @native_libs, $item[0];
      } else {
        push @libs_32, $item[0];
      }
    }
  }
}

=head1 EXIT CODES

Solibs.pm subroutines can return the following exit codes:

  _ERR_SCRIPT        2   script or module bug
  _ERR_OPENFH        6   failure to open file handles

=head1 SEE ALSO

SBO::Lib(3), SBO::Lib::Build(3), SBO::Lib::Download(3), SBO::Lib::Info(3), SBO::Lib::Pkgs(3), SBO::Lib::Readme(3), SBO::Lib::Repo(3), SBO::Lib::Tree(3), SBO::Lib::Util(3), elf(5), ldconfig(1)

In addition to the man page, C<https://refspecs.linuxbase.org/elf/gabi4+/> is a helpful
resource about the structure of ELF files.

=head1 AUTHORS

SBO::Lib was originally written by Jacob Pipkin <jacob.pipkin@icloud.com> with
contributions from Luke Williams <xocel@iquidus.org> and Andreas
Guldstrand <andreas.guldstrand@gmail.com>.

=head1 MAINTAINER

SBO::Lib is maintained by K. Eugene Carlson <kvngncrlsn@gmail.com>.

=head1 LICENSE

The sbotools are licensed under the MIT License.

Copyright (C) 2012-2017, Jacob Pipkin, Luke Williams, Andreas Guldstrand.

Copyright (C) 2024-2026, K. Eugene Carlson.

Copyright (C) 2026, K. Eugene Carlson, Jacob Pipkin.

=cut

sub _caught_signal {
  exit 0;
}

1;
