#!/usr/bin/env perl
use strict;
use warnings;
use 5.020;
use experimental 'signatures';
no warnings 'experimental::signatures';

# see http://www.lightofdawn.org/wiki/wiki.cgi/NewAppsOnOldGlibc for details

# as of currently, only `log2` is being used from GLIBC_2.29, and a
# corresponding version from GLIBC_2.2.5 does exist, so we can just swap it
# out.

my $bin = $ARGV[0];

system("readelf -s $bin | grep -q GLIBC_2.29");
if ($?) {
    die "no symbols from GLIBC_2.29 found, skipping\n";
}

my %readelf_v = parse_readelf_v($bin);
my %readelf_s = parse_readelf_s($bin);

if ($readelf_s{'log2@GLIBC_2.29'}{ver} != $readelf_v{'.gnu.version_r'}{'libm.so.6'}{'GLIBC_2.29'}{ver}) {
    die "log2 doesn't appear to use the symbol version from GLIBC_2.29\n";
}

my $bin_contents = do {
    local $/;
    open my $bin_fh, '<', $bin or die "couldn't open $bin: $!";
    <$bin_fh>
};

my $gnu_version_r_offset = $readelf_v{'.gnu.version_r'}{offset};
my $glibc_229_offset = $gnu_version_r_offset + $readelf_v{'.gnu.version_r'}{'libm.so.6'}{'GLIBC_2.29'}{offset};
my $glibc_225_offset = $gnu_version_r_offset + $readelf_v{'.gnu.version_r'}{'libm.so.6'}{'GLIBC_2.2.5'}{offset};

# we could just make the GLIBC_2.29 symbol weak (as described in the above
# page), but that causes a warning to be printed every time you run the binary,
# which is obnoxious

# use constant VER_FLG_WEAK => 0x02;
# my $prev_glibc_229_flags = substr $bin_contents, $glibc_229_offset + 0x04, 2, pack('s', VER_FLG_WEAK);
# if (unpack('s', $prev_glibc_229_flags) != 0) {
#     die "GLIBC_2.29 did not have flags set to 0\n";
# }

# instead, we overwrite vna_hash, vna_flags, vna_other, vna_name with the 2.2.5
# versions (but leave vna_next the same) - this causes there to be two entries
# describing GLIBC_2.2.5, but this appears to be harmless
substr($bin_contents, $glibc_229_offset + 0x00, 12, substr($bin_contents, $glibc_225_offset + 0x00, 12));

my $gnu_version_offset = $readelf_v{'.gnu.version'}{offset};
my $log2_offset = $gnu_version_offset + 2 * $readelf_s{'log2@GLIBC_2.29'}{num};

my $prev_log2_version = substr $bin_contents, $log2_offset, 2, pack('s', $readelf_v{'.gnu.version_r'}{'libm.so.6'}{'GLIBC_2.2.5'}{ver});
if (unpack('s', $prev_log2_version) != $readelf_v{'.gnu.version_r'}{'libm.so.6'}{'GLIBC_2.29'}{ver}) {
    die "log2 doesn't appear to use the symbol version from GLIBC_2.29\n";
}

open my $fh, '>', "$bin.new" or die "couldn't open $bin.new: $!";
print $fh $bin_contents;
chmod 0755, "$bin.new";

my $remaining = `readelf -s $bin.new | grep GLIBC_2.29`;
if (!$?) {
    die "additional symbols from GLIBC_2.29 found:\n$remaining\n";
}

sub parse_readelf_s($bin) {
    my $readelf = `readelf -s $bin`;

    my %ret;
    for my $line (split "\n", $readelf) {
        if ($line =~ /^ *[0-9]+:/) {
            my ($num, $value, $size, $type, $bind, $vis, $ndx, $name, $ver) = split ' ', $line;
            if (defined $ver) {
                ($ver) = $ver =~ /\((.*)\)/;
            }
            if (defined $num) {
                ($num) = $num =~ /(.*):/;
            }
            $ret{$name} = {
                num => $num,
                ver => $ver,
            }
        }
    }

    %ret
}

sub parse_readelf_v($bin) {
    my $readelf = `readelf -V $bin`;

    my $section;
    my $file;
    my %ret;
    for my $line (split "\n", $readelf) {
        if ($line =~ /^Version (?:symbols|needs) section '([^']+)'/) {
            $section = $1;
        }
        if ($line =~ /^ Addr: (?:[^ ]+)  Offset: ([^ ]+)/) {
            $ret{$section}{offset} = hex($1);
        }
        if ($line =~ /^  [^:]+: Version: [^ ]+  File: ([^ ]+)/) {
            $file = $1;
        }
        if ($line =~ /^  ([^:]+):   Name: ([^ ]+)  Flags: (?:[^ ]+)  Version: (.*)/) {
            $ret{$section}{$file}{$2} = {
                offset => hex($1),
                ver => $3,
            };
        }
    }

    %ret
}
