#! /usr/bin/perl
# Written by Zack Weinberg <zackw at panix.com> in 2018 and 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.

# Generate crypt-hashes.h from lib/hashes.conf and configure settings.
# Also responsible for checking lib/hashes.conf for errors.
#
# Caution: if you change the format of lib/hashes.conf you will
# probably need to modify expand-selected-hashes as well as this script.

use v5.14;    # implicit use strict, use feature ':5.14'
use warnings FATAL => 'all';
use utf8;
use open qw(:std :utf8);
no  if $] >= 5.022, warnings => 'experimental::re_strict';
use if $] >= 5.022, re       => 'strict';

use File::Spec::Functions qw(splitpath);
use FindBin ();
use lib $FindBin::Bin;
use BuildCommon qw(
    enabled_set
    parse_hashes_conf
);

sub output {
    my ($basehc, $hashes_enabled, $hconf) = @_;
    my %enabled = enabled_set($hashes_enabled);
    my @enabled_hashes;

    print <<"EOT";
/* Generated by ${FindBin::Script} from $basehc.  DO NOT EDIT.  */

#ifndef _CRYPT_HASHES_H
#define _CRYPT_HASHES_H 1

EOT
    for my $e (sort { $a->name cmp $b->name } values %{$hconf->hashes}) {
        my $name = $e->name;
        my $ena  = $enabled{$name} // 0;
        printf "#define INCLUDE_%-*s %d\n", $hconf->max_namelen, $name, $ena;
        push @enabled_hashes, $e if $ena;
    }

    print <<'EOT';

/* Internal symbol renames for static linkage, see crypt-port.h.  */
EOT
    for my $e (@enabled_hashes) {
        my $name_rn = $e->name . '_rn';
        printf "#define crypt_%-*s _crypt_crypt_%s\n",
            $hconf->max_namelen + 5, $name_rn, $name_rn;
        printf "#define gensalt_%-*s _crypt_gensalt_%s\n",
            $hconf->max_namelen + 3, $name_rn, $name_rn;
    }

    print <<'EOT';

/* Prototypes for hash algorithm entry points.  */
EOT
    for my $e (@enabled_hashes) {
        my $name = $e->name;
        print <<"EOT";
extern void crypt_${name}_rn (const char *, size_t, const char *,
                size_t, uint8_t *, size_t, void *, size_t);
extern void gensalt_${name}_rn (unsigned long,
                const uint8_t *, size_t, uint8_t *, size_t);

EOT
    }

    print <<'EOT';
#define HASH_ALGORITHM_TABLE_ENTRIES \
EOT
    # Entries in this table can be in any order _except_ that the hash
    # whose prefix is the empty string, if it's enabled, must be last.
    # The simplest way to accomplish this is to sort the prefixes in
    # descending order of length (and then alphabetically as a
    # tiebreaker).
    my @table_hashes = sort {
        -(length($a->prefix) <=> length($b->prefix))
            || $a->prefix cmp $b->prefix
    } @enabled_hashes;

    for my $e (@table_hashes) {
        my $name_rn     = $e->name . '_rn,';
        my $q_prefix    = '"' . $e->prefix . '",';
        my $str_nrbytes = $e->nrbytes . ',';
        printf "  { %-*s %d, crypt_%-*s gensalt_%-*s %-*s %s}, \\\n",
            $hconf->max_prefixlen + 3,  $q_prefix, length($e->prefix),
            $hconf->max_namelen + 4,    $name_rn,
            $hconf->max_namelen + 4,    $name_rn,
            $hconf->max_nrbyteslen + 1, $str_nrbytes, $e->is_strong;
    }
    print "  { 0, 0, 0, 0, 0, 0 }\n";

    # The default_candidates array is in decreasing order of strength;
    # select the first one that's enabled, if any.
    my $default_prefix;
    for my $e (@{$hconf->default_candidates}) {
        if ($enabled{$e->name}) {
            $default_prefix = $e->prefix;
            last;
        }
    }
    print <<"EOT" if defined $default_prefix;

#define HASH_ALGORITHM_DEFAULT "$default_prefix"
EOT
    print <<'EOT';

#endif /* crypt-hashes.h */
EOT

    close STDOUT or die "write error: $!\n";
    return;
}

#
# Main
#
if (scalar(@ARGV) != 2) {
    print {*STDERR} "usage: ${FindBin::Script}"
        . "hashes.conf ,names,of,enabled,hashes,\n";
    exit 1;
}
exit 0 if eval {
    my ($hashes_conf, $hashes_enabled) = @ARGV;
    my (undef, undef, $basehc) = splitpath($hashes_conf);

    my $hconf = parse_hashes_conf($hashes_conf);
    output($basehc, $hashes_enabled, $hconf);
    1;
};

print {*STDERR} "${FindBin::Script}: $@";
exit 1;
