#! /usr/bin/env perl
# Copyright 2018-2022 The OpenSSL Project Authors. All Rights Reserved.
#
# Licensed under the Apache License 2.0 (the "License").  You may not use
# this file except in compliance with the License.  You can obtain a copy
# in the file LICENSE in the source distribution or at
# https://www.openssl.org/source/license.html

# Generate a linker version script suitable for the given platform
# from a given ordinals file.

use strict;
use warnings;

use Getopt::Long;
use FindBin;
use lib "$FindBin::Bin/perl";

use OpenSSL::Ordinals;

use lib '.';
use configdata;

use File::Spec::Functions;
use lib catdir($config{sourcedir}, 'Configurations');
use platform;

my $name = undef;               # internal library/module name
my $ordinals_file = undef;      # the ordinals file to use
my $version = undef;            # the version to use for the library
my $OS = undef;                 # the operating system family
my $type = 'lib';               # either lib or dso
my $verbose = 0;
my $ctest = 0;
my $debug = 0;

# For VMS, some modules may have case insensitive names
my $case_insensitive = 0;

GetOptions('name=s'     => \$name,
           'ordinals=s' => \$ordinals_file,
           'version=s'  => \$version,
           'OS=s'       => \$OS,
           'type=s'     => \$type,
           'ctest'      => \$ctest,
           'verbose'    => \$verbose,
           # For VMS
           'case-insensitive' => \$case_insensitive)
    or die "Error in command line arguments\n";

die "Please supply arguments\n"
    unless $name && $ordinals_file && $OS;
die "--type argument must be equal to 'lib' or 'dso'"
    if $type ne 'lib' && $type ne 'dso';

# When building a "variant" shared library, with a custom SONAME, also customize
# all the symbol versions.  This produces a shared object that can coexist
# without conflict in the same address space as a default build, or an object
# with a different variant tag.
#
# For example, with a target definition that includes:
#
#         shlib_variant => "-opt",
#
# we build the following objects:
#
# $ perl -le '
#     for (@ARGV) {
#         if ($l = readlink) {
#             printf "%s -> %s\n", $_, $l
#         } else {
#             print
#         }
#     }' *.so*
# libcrypto-opt.so.1.1
# libcrypto.so -> libcrypto-opt.so.1.1
# libssl-opt.so.1.1
# libssl.so -> libssl-opt.so.1.1
#
# whose SONAMEs and dependencies are:
#
# $ for l in *.so; do
#     echo $l
#     readelf -d $l | egrep 'SONAME|NEEDED.*(ssl|crypto)'
#   done
# libcrypto.so
#  0x000000000000000e (SONAME)             Library soname: [libcrypto-opt.so.1.1]
# libssl.so
#  0x0000000000000001 (NEEDED)             Shared library: [libcrypto-opt.so.1.1]
#  0x000000000000000e (SONAME)             Library soname: [libssl-opt.so.1.1]
#
# We case-fold the variant tag to uppercase and replace all non-alnum
# characters with "_".  This yields the following symbol versions:
#
# $ nm libcrypto.so | grep -w A
# 0000000000000000 A OPENSSL_OPT_1_1_0
# 0000000000000000 A OPENSSL_OPT_1_1_0a
# 0000000000000000 A OPENSSL_OPT_1_1_0c
# 0000000000000000 A OPENSSL_OPT_1_1_0d
# 0000000000000000 A OPENSSL_OPT_1_1_0f
# 0000000000000000 A OPENSSL_OPT_1_1_0g
# $ nm libssl.so | grep -w A
# 0000000000000000 A OPENSSL_OPT_1_1_0
# 0000000000000000 A OPENSSL_OPT_1_1_0d
#
(my $SO_VARIANT = uc($target{"shlib_variant"} // '')) =~ s/\W/_/g;

my $libname = $type eq 'lib' ? platform->sharedname($name) : platform->dsoname($name);

my %OS_data = (
    solaris     => { writer     => \&writer_linux,
                     sort       => sorter_linux(),
                     platforms  => { UNIX                       => 1 } },
    "solaris-gcc" => 'solaris', # alias
    linux       => 'solaris',   # alias
    "bsd-gcc"   => 'solaris',   # alias
    aix         => { writer     => \&writer_aix,
                     sort       => sorter_unix(),
                     platforms  => { UNIX                       => 1 } },
    VMS         => { writer     => \&writer_VMS,
                     sort       => OpenSSL::Ordinals::by_number(),
                     platforms  => { VMS                        => 1 } },
    vms         => 'VMS',       # alias
    WINDOWS     => { writer     => \&writer_windows,
                     sort       => OpenSSL::Ordinals::by_name(),
                     platforms  => { WIN32                      => 1,
                                     _WIN32                     => 1 } },
    windows     => 'WINDOWS',   # alias
    WIN32       => 'WINDOWS',   # alias
    win32       => 'WIN32',     # alias
    32          => 'WIN32',     # alias
    NT          => 'WIN32',     # alias
    nt          => 'WIN32',     # alias
    mingw       => 'WINDOWS',   # alias
    nonstop     => { writer     => \&writer_nonstop,
                     sort       => OpenSSL::Ordinals::by_name(),
                     platforms  => { TANDEM                     => 1 } },
   );

do {
    die "Unknown operating system family $OS\n"
        unless exists $OS_data{$OS};
    $OS = $OS_data{$OS};
} while(ref($OS) eq '');

my %disabled_uc = map { my $x = uc $_; $x =~ s|-|_|g; $x => 1 } keys %disabled;

my %ordinal_opts = ();
$ordinal_opts{sort} = $OS->{sort} if $OS->{sort};
$ordinal_opts{filter} =
    sub {
        my $item = shift;
        return
            $item->exists()
            && platform_filter($item)
            && feature_filter($item);
    };
my $ordinals = OpenSSL::Ordinals->new(from => $ordinals_file);

my $writer = $OS->{writer};
$writer = \&writer_ctest if $ctest;

$writer->($ordinals->items(%ordinal_opts));

exit 0;

sub platform_filter {
    my $item = shift;
    my %platforms = ( $item->platforms() );

    # True if no platforms are defined
    return 1 if scalar keys %platforms == 0;

    # For any item platform tag, return the equivalence with the
    # current platform settings if it exists there, return 0 otherwise
    # if the item platform tag is true
    for (keys %platforms) {
        if (exists $OS->{platforms}->{$_}) {
            return $platforms{$_} == $OS->{platforms}->{$_};
        }
        if ($platforms{$_}) {
            return 0;
        }
    }

    # Found no match?  Then it's a go
    return 1;
}

sub feature_filter {
    my $item = shift;
    my @features = ( $item->features() );

    # True if no features are defined
    return 1 if scalar @features == 0;

    my $verdict = ! grep { $disabled_uc{$_} } @features;

    if ($disabled{deprecated}) {
        foreach (@features) {
            next unless /^DEPRECATEDIN_(\d+)_(\d+)(?:_(\d+))?$/;
            my $symdep = $1 * 10000 + $2 * 100 + ($3 // 0);
            $verdict = 0 if $config{api} >= $symdep;
            print STDERR "DEBUG: \$symdep = $symdep, \$verdict = $verdict\n"
                if $debug && $1 == 0;
        }
    }

    return $verdict;
}

sub sorter_unix {
    my $by_name = OpenSSL::Ordinals::by_name();
    my %weight = (
        'FUNCTION'      => 1,
        'VARIABLE'      => 2
       );

    return sub {
        my $item1 = shift;
        my $item2 = shift;

        my $verdict = $weight{$item1->type()} <=> $weight{$item2->type()};
        if ($verdict == 0) {
            $verdict = $by_name->($item1, $item2);
        }
        return $verdict;
    };
}

sub sorter_linux {
    my $by_version = OpenSSL::Ordinals::by_version();
    my $by_unix = sorter_unix();

    return sub {
        my $item1 = shift;
        my $item2 = shift;

        my $verdict = $by_version->($item1, $item2);
        if ($verdict == 0) {
            $verdict = $by_unix->($item1, $item2);
        }
        return $verdict;
    };
}

sub writer_linux {
    my $thisversion = '';
    my $currversion_s = '';
    my $prevversion_s = '';
    my $indent = 0;

    for (@_) {
        if ($thisversion && $_->version() ne $thisversion) {
            die "$ordinals_file: It doesn't make sense to have both versioned ",
                "and unversioned symbols"
                if $thisversion eq '*';
            print <<"_____";
}${prevversion_s};
_____
            $prevversion_s = " OPENSSL${SO_VARIANT}_$thisversion";
            $thisversion = '';  # Trigger start of next section
        }
        unless ($thisversion) {
            $indent = 0;
            $thisversion = $_->version();
            $currversion_s = '';
            $currversion_s = "OPENSSL${SO_VARIANT}_$thisversion "
                if $thisversion ne '*';
            print <<"_____";
${currversion_s}{
    global:
_____
        }
        print '        ', $_->name(), ";\n";
    }

    print <<"_____";
    local: *;
}${prevversion_s};
_____
}

sub writer_aix {
    for (@_) {
        print $_->name(),"\n";
    }
}

sub writer_nonstop {
    for (@_) {
        print "-export ",$_->name(),"\n";
    }
}

sub writer_windows {
    print <<"_____";
;
; Definition file for the DLL version of the $libname library from OpenSSL
;

LIBRARY         "$libname"

EXPORTS
_____
    for (@_) {
        print "    ",$_->name();
        if (platform->can('export2internal')) {
            print "=". platform->export2internal($_->name());
        }
        print "\n";
    }
}

sub collect_VMS_mixedcase {
    return [ 'SPARE', 'SPARE' ] unless @_;

    my $s = shift;
    my $s_uc = uc($s);
    my $type = shift;

    return [ "$s=$type", 'SPARE' ] if $s_uc eq $s;
    return [ "$s_uc/$s=$type", "$s=$type" ];
}

sub collect_VMS_uppercase {
    return [ 'SPARE' ] unless @_;

    my $s = shift;
    my $s_uc = uc($s);
    my $type = shift;

    return [ "$s_uc=$type" ];
}

sub writer_VMS {
    my @slot_collection = ();
    my $collector =
        $case_insensitive ? \&collect_VMS_uppercase : \&collect_VMS_mixedcase;

    my $last_num = 0;
    foreach (@_) {
        my $this_num = $_->number();
        $this_num = $last_num + 1 if $this_num =~ m|^\?|;

        while (++$last_num < $this_num) {
            push @slot_collection, $collector->(); # Just occupy a slot
        }
        my $type = {
            FUNCTION    => 'PROCEDURE',
            VARIABLE    => 'DATA'
           } -> {$_->type()};
        push @slot_collection, $collector->($_->name(), $type);
    }

    print <<"_____" if defined $version;
IDENTIFICATION=$version
_____
    print <<"_____" unless $case_insensitive;
CASE_SENSITIVE=YES
_____
    print <<"_____";
SYMBOL_VECTOR=(-
_____
    # It's uncertain how long aggregated lines the linker can handle,
    # but it has been observed that at least 1024 characters is ok.
    # Either way, this means that we need to keep track of the total
    # line length of each "SYMBOL_VECTOR" statement.  Fortunately, we
    # can have more than one of those...
    my $symvtextcount = 16;     # The length of "SYMBOL_VECTOR=("
    while (@slot_collection) {
        my $set = shift @slot_collection;
        my $settextlength = 0;
        foreach (@$set) {
            $settextlength +=
                + 3             # two space indentation and comma
                + length($_)
                + 1             # postdent
                ;
        }
        $settextlength--;       # only one space indentation on the first one
        my $firstcomma = ',';

        if ($symvtextcount + $settextlength > 1024) {
            print <<"_____";
)
SYMBOL_VECTOR=(-
_____
            $symvtextcount = 16; # The length of "SYMBOL_VECTOR=("
        }
        if ($symvtextcount == 16) {
            $firstcomma = '';
        }

        my $indent = ' '.$firstcomma;
        foreach (@$set) {
            print <<"_____";
$indent$_ -
_____
            $symvtextcount += length($indent) + length($_) + 1;
            $indent = '  ,';
        }
    }
    print <<"_____";
)
_____

    if (defined $version) {
        $version =~ /^(\d+)\.(\d+)\.(\d+)/;
        my $libvmajor = $1;
        my $libvminor = $2 * 100 + $3;
        print <<"_____";
GSMATCH=LEQUAL,$libvmajor,$libvminor
_____
    }
}

sub writer_ctest {
    print <<'_____';
/*
 * Test file to check all DEF file symbols are present by trying
 * to link to all of them. This is *not* intended to be run!
 */

int main()
{
_____

    my $last_num = 0;
    for (@_) {
        my $this_num = $_->number();
        $this_num = $last_num + 1 if $this_num =~ m|^\?|;

        if ($_->type() eq 'VARIABLE') {
            print "\textern int ", $_->name(), '; /* type unknown */ /* ',
                  $this_num, ' ', $_->version(), " */\n";
        } else {
            print "\textern int ", $_->name(), '(); /* type unknown */ /* ',
                  $this_num, ' ', $_->version(), " */\n";
        }

        $last_num = $this_num;
    }
    print <<'_____';
}
_____
}
