# Written by Zack Weinberg 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;