# Written by Zack Weinberg <zackw at panix.com> in 2020.
# To the extent possible under law, Zack Weinberg has waived all
# copyright and related or neighboring rights to this work.
#
# See https://creativecommons.org/publicdomain/zero/1.0/ for further
# details.

# Code shared among all of the Perl-language tests in this directory.

package TestCommon;

use v5.14;    # implicit use strict, use feature ':5.14'
use warnings FATAL => 'all';
use utf8;
use open qw(:utf8);

no  if $] >= 5.022, warnings => 'experimental::re_strict';
use if $] >= 5.022, re       => 'strict';

use Cwd qw(realpath);
use File::Spec::Functions qw(
    catdir
    catpath
    splitpath
);
use FindBin ();
use POSIX   ();

use lib "$FindBin::Bin/../build-aux";
## ProhibitUnusedImport does not notice uses from @EXPORT_OK.
## no critic (TooMuchCode::ProhibitUnusedImport)
use BuildCommon qw(
    ensure_C_locale
    error
    popen
    sh_split
    sh_quote
    subprocess_error
    which
);
## use critic

our @EXPORT_OK;
use Exporter qw(import);

BEGIN {
    # Re-export all the subprocess handling routines from BuildCommon
    # as a convenience for individual tests.
    @EXPORT_OK = qw(
        compare_symbol_lists
        ensure_C_locale
        error
        fail
        find_real_library
        get_symbols
        popen
        sh_quote
        sh_split
        skip
        subprocess_error
        which
    );
}

# Diagnostics: report that the test has failed.
sub fail {    ## no critic (Subroutines::RequireArgUnpacking)
    my $msg = join q{ }, @_;
    print {*STDERR} $FindBin::Script, ': FAIL: ', $msg, "\n";
    exit 1;
}

# Diagnostics: report that the test should be 'skipped' because
# some piece of infrastructure we need is missing.
sub skip {    ## no critic (Subroutines::RequireArgUnpacking)
    my $msg = join q{ }, @_;
    print {*STDERR} $FindBin::Script, ': skipping test: ', $msg, "\n";
    exit 77;
}

# Parse a .la file (arg 1) and determine the name of the actual .a or
# .so file it refers to (arg 2: 'static' for .a, 'shared' for .so)
sub find_real_library {
    my ($lib_la, $type) = @_;

    state @SH;
    if (!@SH) {
        @SH = which($ENV{SHELL} || $ENV{CONFIG_SHELL} || '/bin/sh');
        error('no shell available???') if !@SH;
    }

    my $param;
    if ($type eq 'shared') {
        $param = 'dlname';
    } elsif ($type eq 'static') {
        $param = 'old_library';
    } else {
        error("unknown library type: '$type'");
    }

    # We're going to interpolate $lib_la into a shell command.
    # Save the unmangled directory part first, then quote it.
    my ($vol, $dir, undef) = splitpath($lib_la);
    $lib_la = sh_quote($lib_la);

    # .la files are shell script fragments.  The easiest way to learn
    # the name of the actual library is to ask a shell to parse the
    # fragment for us.
    my $fh = popen('-|', @SH, '-c', ". $lib_la; printf %s \"\$$param\"");
    my $real_library;
    {
        local $/ = undef;    # slurp
        $real_library = <$fh>;
    }
    close $fh or subprocess_error($SH[0]);

    chomp $real_library;
    $real_library = catpath($vol, catdir($dir, '.libs'), $real_library);
    error("'$real_library' does not exist") unless -f $real_library;
    return realpath($real_library);
}

# In some object file formats, all symbols defined in C have an
# underscore prepended to their names.  The configure script detects
# this and the Makefiles set this environment variable appropriately.
my $symbol_prefix = $ENV{SYMBOL_PREFIX} || q{};

# Return a hashset of symbols exported by the library $_[0], using readelf.
# If it is a dynamic library, annotate each symbol with its version tag.
sub get_symbols_readelf {
    my $lib    = shift;
    my $filter = shift // sub { 1 };

    state $readelf_works = 1;
    die "readelf doesn't work\n" unless $readelf_works;

    state @READELF;
    if (!@READELF) {
        @READELF = which($ENV{READELF} || 'readelf');
        die "readelf not available\n" unless @READELF;
    }

    my @opts              = ('--wide');
    my $want_version_tags = 0;
    if ($lib =~ /\.(?:a|lib)$/) {
        push @opts, '--syms';
    } else {
        push @opts, '--dyn-syms';
        $want_version_tags = 1;
    }

    my $fh = popen('-|', @READELF, @opts, $lib);

    local $_;
    my %symbols;
    my $saw_version_tags = 0;
    while (<$fh>) {
        chomp;
        s/\s+$//;
        next if /^(?:$|File:|Symbol table)/;
        next if /^\s*Num:\s+Value\s+Size\s+Type\s+Bind\s+Vis\s+Ndx\s+Name$/;

        my ($num, $value, $size, $type, $bind, $vis, $ndx, $name) = split;

        # We are only interested in globally visible, defined,
        # non-absolute symbols.
        next
            if $ndx eq 'UND'
            || $ndx eq 'ABS'
            || $bind eq 'LOCAL';

        # Strip the symbol prefix, if any, from each symbol.
        $name =~ s/^$symbol_prefix// if $symbol_prefix ne q{};

        $saw_version_tags = 1 if $name =~ /@[A-Z_]+[0-9]/;

        if (&{$filter}($name)) {
            print {*STDERR} "|+ $name\n";
            $symbols{$name} = 1;
        } else {
            print {*STDERR} "|- $name\n";
        }
    }
    if (!close $fh) {
        # If it ran but exited 1 or 2, don't give up yet, we still
        # have nm to try.
        if ($! == 0 && ($? == 256 || $? == 512)) {
            $readelf_works = 0;
            die "$READELF[0] exited " . ($? >> 2) . "\n";
        }
        subprocess_error($READELF[0]);
    }
    if ($want_version_tags && !$saw_version_tags) {
        $readelf_works = 0;
        die "$READELF[0] did not print version tags\n";
    }
    return \%symbols;
}

# Return a hashset of symbols exported by the library $_[0], using nm.
# If it is a dynamic library, annotate each symbol with its version tag.
sub get_symbols_nm {
    my $lib    = shift;
    my $filter = shift // sub { 1 };

    state $nm_works = 1;
    die "nm doesn't work\n" unless $nm_works;

    state @NM;
    if (!@NM) {
        @NM = which($ENV{NM} || 'nm');
        die "nm not available\n" unless @NM;
    }

    my @opts              = qw(--format=bsd --extern-only --defined-only);
    my $want_version_tags = 0;
    if ($lib !~ /\.(?:a|lib)$/) {
        push @opts, qw(--dynamic --with-symbol-versions);
        $want_version_tags = 1;
    }

    my $fh = popen('-|', @NM, @opts, $lib);
    local $_;
    my %symbols;
    my $saw_version_tags = 0;
    while (<$fh>) {
        chomp;
        s/\s+$//;
        next unless $_;

        # BSD-format nm output, when restricted to external, defined
        # symbols, has three fields per line: address type name.
        # We shouldn't ever see symbols with the address field blank,
        # but just in case, discard them.
        next unless /^([0-9a-fA-F]+)\s+([A-Za-z])\s+(\S+)$/;
        my $addr = $1;
        my $type = $2;
        my $name = $3;

        # Symbols whose address is 0 and type is A are uninteresting;
        # they define the set of symbol version tags.
        next if $addr =~ /^0+$/ && $type eq 'A';

        # Strip the symbol prefix, if any, from each symbol.
        $name =~ s/^$symbol_prefix// if $symbol_prefix;

        # Compensate for a bug in some versions of GNU nm
        # where the symbol version is printed twice.
        $name =~ s/(@+[A-Z0-9_.]+)\1$/$1/;

        $saw_version_tags = 1 if $name =~ /@[A-Z_]+[0-9]/;

        if (&{$filter}($name)) {
            print {*STDERR} "|+ $name\n";
            $symbols{$name} = 1;
        } else {
            print {*STDERR} "|- $name\n";
        }
    }
    if (!close $fh) {
        # If it ran but exited 1 or 2, don't give up yet, we still
        # have readelf to try.
        if ($! == 0 && ($? == 256 || $? == 512)) {
            $nm_works = 0;
            die "$NM[0] exited " . ($? >> 8) . "\n";
        }
        subprocess_error($NM[0]);
    }
    if ($want_version_tags && !$saw_version_tags) {
        $nm_works = 0;
        die "$NM[0] did not print version tags\n";
    }
    return \%symbols;
}

# Return a hashset of symbols exported by the library $_[0], using
# readelf or nm, whichever works on this system.  If it is a dynamic
# library, annotate each symbol with its version tag.  If $_[1] is
# defined, it is a filter procedure; only symbols for which the filter
# returns true are included in the hashset.
sub get_symbols {    ## no critic (Subroutines::RequireArgUnpacking)
    my $result;

    $result = eval { get_symbols_nm(@_); };
    return $result if $result;
    print {*STDERR} "get_symbols_nm: $@";

    $result = eval { get_symbols_readelf(@_); };
    return $result if $result;
    print {*STDERR} "get_symbols_readelf: $@";

    skip('cannot get symbols using either readelf or nm');
}

sub compare_symbol_lists {
    my ($found, $expected, $tag, $extra_allowed) = @_;
    my @extra;
    my @missing;
    local $_;
    for (keys %{$expected}) {
        push @missing, $_ unless exists $found->{$_};
    }
    for (keys %{$found}) {
        push @extra, $_ unless exists $expected->{$_};
    }

    my $error = 0;
    if (@extra) {
        $error = 1 unless $extra_allowed;
        print {*STDERR} "*** Extra $tag:\n";
        for (sort @extra) {
            s/^_crypt_//;
            print {*STDERR} "  $_\n";
        }
    }
    if (@missing) {
        $error = 1;
        print {*STDERR} "*** Missing $tag:\n";
        for (sort @missing) {
            s/^_crypt_//;
            print {*STDERR} "  $_\n";
        }
    }
    return $error;
}

1;
