#! /usr/bin/env perl
# Copyright 2018-2024 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

use strict;
no strict 'refs';               # To be able to use strings as function refs
use OpenSSL::Test;
use OpenSSL::Test::Utils;
use Errno qw(:POSIX);
use POSIX qw(:limits_h strerror);

use Data::Dumper;

setup('test_errstr');

# In a cross compiled situation, there are chances that our
# application is linked against different C libraries than
# perl, and may thereby get different error messages for the
# same error.
# The safest is not to test under such circumstances.
plan skip_all => 'This is unsupported for cross compiled configurations'
    if config('CROSS_COMPILE');

# The same can be said when compiling OpenSSL with mingw configuration
# on Windows when built with msys perl.  Similar problems are also observed
# in MSVC builds, depending on the perl implementation used.
plan skip_all => 'This is unsupported on MSYS/MinGW or MSWin32'
    if $^O eq 'msys' or $^O eq 'MSWin32';

plan skip_all => 'OpenSSL is configured "no-autoerrinit" or "no-err"'
    if disabled('autoerrinit') || disabled('err');

# OpenSSL constants found in <openssl/err.h>
use constant ERR_SYSTEM_FLAG => INT_MAX + 1;
use constant ERR_LIB_OFFSET => 23; # Offset of the "library" errcode section

# OpenSSL "library" numbers
use constant ERR_LIB_NONE => 1;

# We use Errno::EXPORT_OK as a list of known errno values on the current
# system.  libcrypto's ERR should either use the same string as perl, or if
# it was outside the range that ERR looks at, ERR gives the reason string
# "reason(nnn)", where nnn is the errno number.

plan tests => scalar @Errno::EXPORT_OK
    +1                          # Checking that error 128 gives 'reason(128)'
    +1                          # Checking that error 0 gives the library name
    +1;                         # Check trailing whitespace is removed.

# Test::More:ok() has a sub prototype, which means we need to use the '&ok'
# syntax to force it to accept a list as a series of arguments.

foreach my $errname (@Errno::EXPORT_OK) {
    # The error names are perl constants, which are implemented as functions
    # returning the numeric value of that name.
    my $errcode = "Errno::$errname"->();

  SKIP: {
      # On most systems, there is no E macro for errcode zero in <errno.h>,
      # which means that it seldom comes up here.  However, reports indicate
      # that some platforms do have an E macro for errcode zero.
      # With perl, errcode zero is a bit special.  Perl consistently gives
      # the empty string for that one, while the C strerror() may give back
      # something else.  The easiest way to deal with that possible mismatch
      # is to skip this errcode.
      skip "perl error strings and ssystem error strings for errcode 0 differ", 1
          if $errcode == 0;
      # On some systems (for example Hurd), there are negative error codes.
      # These are currently unsupported in OpenSSL error reports.
      skip "negative error codes are not supported in OpenSSL", 1
          if $errcode < 0;

      &ok(match_syserr_reason($errcode));
    }
}

# OpenSSL library 1 is the "unknown" library
&ok(match_opensslerr_reason(ERR_LIB_NONE << ERR_LIB_OFFSET | 256,
                            "reason(256)"));
# Reason code 0 of any library gives the library name as reason
&ok(match_opensslerr_reason(ERR_LIB_NONE << ERR_LIB_OFFSET |   0,
                            "unknown library"));
&ok(match_any("Trailing whitespace  \n\t", "?", ( "Trailing whitespace" )));

exit 0;

# For an error string "error:xxxxxxxx:lib:func:reason", this returns
# the following array:
#
# ( "xxxxxxxx", "lib", "func", "reason" )
sub split_error {
    # Limit to 5 items, in case the reason contains a colon
    my @erritems = split /:/, $_[0], 5;

    # Remove the first item, which is always "error"
    shift @erritems;

    return @erritems;
}

# Compares the first argument as string to each of the arguments 3 and on,
# and returns an array of two elements:
# 0:  True if the first argument matched any of the others, otherwise false
# 1:  A string describing the test
# The returned array can be used as the arguments to Test::More::ok()
sub match_any {
    my $first = shift;
    my $desc = shift;
    my @strings = @_;

    # ignore trailing whitespace
    $first =~ s/\s+$//;

    if (scalar @strings > 1) {
        $desc = "match '$first' ($desc) with one of ( '"
            . join("', '", @strings) . "' )";
    } else {
        $desc = "match '$first' ($desc) with '$strings[0]'";
    }

    return ( scalar(
                 grep { ref $_ eq 'Regexp' ? $first =~ $_ : $first eq $_ }
                 @strings
             ) > 0,
             $desc );
}

sub match_opensslerr_reason {
    my $errcode = shift;
    my @strings = @_;

    my $errcode_hex = sprintf "%x", $errcode;
    my @res = run(app([ qw(openssl errstr), $errcode_hex ]), capture => 1);
    return 0 unless $#res >= 0;
    my $reason = $res[0];
    $reason =~ s|\R$||;
    $reason = ( split_error($reason) )[3];

    return match_any($reason, $errcode_hex, @strings);
}

sub match_syserr_reason {
    my $errcode = shift;

    my @strings = ();
    # The POSIX reason string
    push @strings, eval {
          # Set $! to the error number...
          local $! = $errcode;
          # ... and $! will give you the error string back
          $!
    };
    # Occasionally, we get an error code that is simply not translatable
    # to POSIX semantics on VMS, and we get an error string saying so.
    push @strings, qr/^non-translatable vms error code:/ if $^O eq 'VMS';
    # The OpenSSL fallback string
    push @strings, "reason($errcode)";

    return match_opensslerr_reason(ERR_SYSTEM_FLAG | $errcode, @strings);
}
