/*++
/* NAME
/*	compat_level 3
/* SUMMARY
/*	compatibility_level support
/* SYNOPSIS
/*	#include <compat_level.h>
/*
/*	void compat_level_relop_register(void)
/*
/*	long	compat_level_from_string(
/*	const char *str,
/*	void	PRINTFLIKE(1, 2) (*msg_fn)(const char *,...))
/*
/*	long	compat_level_from_numbers(
/*	long	major,
/*	long	minor,
/*	long	patch,
/*	void	PRINTFLIKE(1, 2) (*msg_fn)(const char *,...))
/*
/*	const char *compat_level_to_string(
/*	long	compat_level,
/*	void	PRINTFLIKE(1, 2) (*msg_fn)(const char *,...))
/* AUXULIARY FUNCTIONS
/*	long	compat_level_from_major_minor(
/*	long	major,
/*	long	minor,
/*	void	PRINTFLIKE(1, 2) (*msg_fn)(const char *,...))
/*
/*	long	compat_level_from_major(
/*	long	major,
/*	void	PRINTFLIKE(1, 2) (*msg_fn)(const char *,...))
/* DESCRIPTION
/*	This module supports compatibility level syntax with
/*	"major.minor.patch" but will also accept the shorter forms
/*	"major.minor" and "major" (missing members default to zero).
/*	Compatibility levels with multiple numbers cannot be compared
/*	as strings or as floating-point numbers (for example, "3.10"
/*	would be smaller than "3.9").
/*
/*	The major number can range from [0..2047] inclusive (11
/*	bits) or more, while the minor and patch numbers can range
/*	from [0..1023] inclusive (10 bits).
/*
/*	compat_level_from_string() converts a compatibility level
/*	from string form to numerical form for easy comparison.
/*	Valid input results in a non-negative result. In case of
/*	error, compat_level_from_string() reports the problem with
/*	the provided function, and returns -1 if that function does
/*	not terminate execution.
/*
/*	compat_level_from_numbers() creates an internal-form
/*	compatibility level from distinct numbers. Valid input
/*	results in a non-negative result. In case of error,
/*	compat_level_from_numbers() reports the problem with the
/*	provided function, and returns -1 if that function does not
/*	terminate execution.
/*
/*	The functions compat_level_from_major_minor() and
/*	compat_level_from_major() are helpers that default the missing
/*	information to zeroes.
/*
/*	compat_level_to_string() converts a compatibility level
/*	from numerical form to canonical string form. Valid input
/*	results in a non-null result. In case of error,
/*	compat_level_to_string() reports the problem with the
/*	provided function, and returns a null pointer if that
/*	function does not terminate execution.
/*
/*	compat_level_relop_register() registers a mac_expand() callback
/*	that registers operators such as <=level, >level, that compare
/*	compatibility levels. This function should be called before
/*	loading parameter settings from main.cf.
/* DIAGNOSTICS
/*	info, .., panic: bad compatibility_level syntax.
/* BUGS
/*	The patch and minor fields range from 0..1023 (10 bits) while
/*	the major field ranges from 0..COMPAT_MAJOR_SHIFT47 or more
/*	(11 bits or more).
/*
/*	This would be a great use case for functions returning
/*	StatusOr<compat_level_t> or StatusOr<string>, but is it a bit
/*	late for a port to C++.
/* LICENSE
/* .ad
/* .fi
/*	The Secure Mailer license must be distributed with this software.
/* AUTHOR(S)
/*	Wietse Venema
/*	Google, Inc.
/*	111 8th Avenue
/*	New York, NY 10011, USA
/*--*/

 /*
  * System library.
  */
#include <sys_defs.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <limits.h>

 /*
  * Utility library.
  */
#include <mac_expand.h>
#include <msg.h>
#include <sane_strtol.h>

 /*
  * For easy comparison we convert a three-number compatibility level into
  * just one number, using different bit ranges for the major version, minor
  * version, and patch level.
  * 
  * We use long integers because standard C guarantees that long has at last 32
  * bits instead of int which may have only 16 bits (though it is unlikely
  * that Postfix would run on such systems). That gives us 11 or more bits
  * for the major version, and 10 bits for minor the version and patchlevel.
  * 
  * Below are all the encoding details in one place. This is easier to verify
  * than wading through code.
  */
#define COMPAT_MAJOR_SHIFT \
	(COMPAT_MINOR_SHIFT + COMPAT_MINOR_WIDTH)

#define COMPAT_MINOR_SHIFT	COMPAT_PATCH_WIDTH
#define COMPAT_MINOR_BITS	0x3ff
#define COMPAT_MINOR_WIDTH	10

#define COMPAT_PATCH_BITS	0x3ff
#define COMPAT_PATCH_WIDTH	10

#define GOOD_MAJOR(m)	((m) >= 0 && (m) <= (LONG_MAX >> COMPAT_MAJOR_SHIFT))
#define GOOD_MINOR(m)	((m) >= 0 && (m) <= COMPAT_MINOR_BITS)
#define GOOD_PATCH(p)	((p) >= 0 && (p) <= COMPAT_PATCH_BITS)

#define ENCODE_MAJOR(m)	((m) << COMPAT_MAJOR_SHIFT)
#define ENCODE_MINOR(m)	((m) << COMPAT_MINOR_SHIFT)
#define ENCODE_PATCH(p)	(p)

#define DECODE_MAJOR(l)	((l) >> COMPAT_MAJOR_SHIFT)
#define DECODE_MINOR(l)	(((l) >> COMPAT_MINOR_SHIFT) & COMPAT_MINOR_BITS)
#define DECODE_PATCH(l)	((l) & COMPAT_PATCH_BITS)

 /*
  * Global library.
  */
#include <compat_level.h>

/* compat_level_from_string - convert major[.minor] to comparable type */

long    compat_level_from_string(const char *str,
		         void PRINTFLIKE(1, 2) (*msg_fn) (const char *,...))
{
    long    major, minor, patch, res = 0;
    const char *start;
    char   *remainder;

    start = str;
    major = sane_strtol(start, &remainder, 10);
    if (start < remainder && (*remainder == 0 || *remainder == '.')
	&& errno != ERANGE && GOOD_MAJOR(major)) {
	res = ENCODE_MAJOR(major);
	if (*remainder == 0)
	    return res;
	start = remainder + 1;
	minor = sane_strtol(start, &remainder, 10);
	if (start < remainder && (*remainder == 0 || *remainder == '.')
	    && errno != ERANGE && GOOD_MINOR(minor)) {
	    res |= ENCODE_MINOR(minor);
	    if (*remainder == 0)
		return (res);
	    start = remainder + 1;
	    patch = sane_strtol(start, &remainder, 10);
	    if (start < remainder && *remainder == 0 && errno != ERANGE
		&& GOOD_PATCH(patch)) {
		return (res | ENCODE_PATCH(patch));
	    }
	}
    }
    msg_fn("malformed compatibility level syntax: \"%s\"", str);
    return (-1);
}

/* compat_level_from_numbers - internal form from numbers */

long    compat_level_from_numbers(long major, long minor, long patch,
		         void PRINTFLIKE(1, 2) (*msg_fn) (const char *,...))
{
    const char myname[] = "compat_level_from_numbers";

    /*
     * Sanity checks.
     */
    if (!GOOD_MAJOR(major)) {
	msg_fn("%s: bad major version: %ld", myname, major);
	return (-1);
    }
    if (!GOOD_MINOR(minor)) {
	msg_fn("%s: bad minor version: %ld", myname, minor);
	return (-1);
    }
    if (!GOOD_PATCH(patch)) {
	msg_fn("%s: bad patch level: %ld", myname, patch);
	return (-1);
    }

    /*
     * Conversion.
     */
    return (ENCODE_MAJOR(major) | ENCODE_MINOR(minor) | ENCODE_PATCH(patch));
}

/* compat_level_to_string - pretty-print a compatibility level */

const char *compat_level_to_string(long compat_level,
		         void PRINTFLIKE(1, 2) (*msg_fn) (const char *,...))
{
    const char myname[] = "compat_level_to_string";
    static VSTRING *buf;
    long    major;
    long    minor;
    long    patch;

    /*
     * Sanity check.
     */
    if (compat_level < 0) {
        msg_fn("%s: bad compatibility level: %ld", myname, compat_level);
        return (0);
    }

    /*
     * Compatibility levels 0..2 have no minor or patch level.
     */
    if (buf == 0)
        buf = vstring_alloc(10);
    major = DECODE_MAJOR(compat_level);
    if (!GOOD_MAJOR(major)) {
        msg_fn("%s: bad compatibility major level: %ld", myname, compat_level);
        return (0);
    }
    vstring_sprintf(buf, "%ld", major);
    if (major > 2) {

        /*
         * Expect that major.minor will be common.
         */
        minor = DECODE_MINOR(compat_level);
        vstring_sprintf_append(buf, ".%ld", minor);

        /*
         * Expect that major.minor.patch will be rare.
         */
        patch = DECODE_PATCH(compat_level);
        if (patch)
            vstring_sprintf_append(buf, ".%ld", patch);
    }
    return (vstring_str(buf));
}

/* compat_relop_eval - mac_expand callback */

static MAC_EXP_OP_RES compat_relop_eval(const char *left_str, int relop,
					        const char *rite_str)
{
    const char myname[] = "compat_relop_eval";
    long    left_val, rite_val, delta;

    /*
     * Negative result means error.
     */
    if ((left_val = compat_level_from_string(left_str, msg_warn)) < 0
	|| (rite_val = compat_level_from_string(rite_str, msg_warn)) < 0)
	return (MAC_EXP_OP_RES_ERROR);

    /*
     * Valid result. The difference between non-negative numbers will no
     * overflow.
     */
    delta = left_val - rite_val;

    switch (relop) {
    case MAC_EXP_OP_TOK_EQ:
	return (mac_exp_op_res_bool[delta == 0]);
    case MAC_EXP_OP_TOK_NE:
	return (mac_exp_op_res_bool[delta != 0]);
    case MAC_EXP_OP_TOK_LT:
	return (mac_exp_op_res_bool[delta < 0]);
    case MAC_EXP_OP_TOK_LE:
	return (mac_exp_op_res_bool[delta <= 0]);
    case MAC_EXP_OP_TOK_GE:
	return (mac_exp_op_res_bool[delta >= 0]);
    case MAC_EXP_OP_TOK_GT:
	return (mac_exp_op_res_bool[delta > 0]);
    default:
	msg_panic("%s: unknown operator: %d",
		  myname, relop);
    }
}

/* compat_level_register - register comparison operators */

void    compat_level_relop_register(void)
{
    int     compat_level_relops[] = {
	MAC_EXP_OP_TOK_EQ, MAC_EXP_OP_TOK_NE,
	MAC_EXP_OP_TOK_GT, MAC_EXP_OP_TOK_GE,
	MAC_EXP_OP_TOK_LT, MAC_EXP_OP_TOK_LE,
	0,
    };
    static int register_done;

    if (register_done++ == 0)
	mac_expand_add_relop(compat_level_relops, "level", compat_relop_eval);
}

#ifdef TEST
#include <unistd.h>

#include <htable.h>
#include <mymalloc.h>
#include <stringops.h>
#include <vstring.h>
#include <vstream.h>
#include <vstring_vstream.h>

static const char *lookup(const char *name, int unused_mode, void *context)
{
    HTABLE *table = (HTABLE *) context;

    return (htable_find(table, name));
}

static void test_expand(void)
{
    VSTRING *buf = vstring_alloc(100);
    VSTRING *result = vstring_alloc(100);
    char   *cp;
    char   *name;
    char   *value;
    HTABLE *table;
    int     stat;

    /*
     * Add relops that compare string lengths instead of content.
     */
    compat_level_relop_register();

    /*
     * Loop over the inputs.
     */
    while (!vstream_feof(VSTREAM_IN)) {

	table = htable_create(0);

	/*
	 * Read a block of definitions, terminated with an empty line.
	 */
	while (vstring_get_nonl(buf, VSTREAM_IN) != VSTREAM_EOF) {
	    vstream_printf("<< %s\n", vstring_str(buf));
	    vstream_fflush(VSTREAM_OUT);
	    if (VSTRING_LEN(buf) == 0)
		break;
	    cp = vstring_str(buf);
	    name = mystrtok(&cp, CHARS_SPACE "=");
	    value = mystrtok(&cp, CHARS_SPACE "=");
	    htable_enter(table, name, value ? mystrdup(value) : 0);
	}

	/*
	 * Read a block of patterns, terminated with an empty line or EOF.
	 */
	while (vstring_get_nonl(buf, VSTREAM_IN) != VSTREAM_EOF) {
	    vstream_printf("<< %s\n", vstring_str(buf));
	    vstream_fflush(VSTREAM_OUT);
	    if (VSTRING_LEN(buf) == 0)
		break;
	    VSTRING_RESET(result);
	    stat = mac_expand(result, vstring_str(buf), MAC_EXP_FLAG_NONE,
			      (char *) 0, lookup, (void *) table);
	    vstream_printf("stat=%d result=%s\n", stat, vstring_str(result));
	    vstream_fflush(VSTREAM_OUT);
	}
	htable_free(table, myfree);
	vstream_printf("\n");
    }

    /*
     * Clean up.
     */
    vstring_free(buf);
    vstring_free(result);
}

static void test_convert(void)
{
    VSTRING *buf = vstring_alloc(100);
    long    compat_level;
    const char *as_string;

    /*
     * Read compatibility level.
     */
    while (vstring_get_nonl(buf, VSTREAM_IN) != VSTREAM_EOF) {
	if ((compat_level = compat_level_from_string(vstring_str(buf),
						     msg_warn)) < 0)
	    continue;
	msg_info("%s -> 0x%lx", vstring_str(buf), compat_level);
	errno = ERANGE;
	if ((as_string = compat_level_to_string(compat_level,
						msg_warn)) == 0)
	    continue;
	msg_info("0x%lx->%s", compat_level, as_string);
    }
    vstring_free(buf);
}

static NORETURN usage(char **argv)
{
    msg_fatal("usage: %s option\n-c (convert)\n-c (expand)", argv[0]);
}

int     main(int argc, char **argv)
{
    int     ch;
    int     mode = 0;

#define MODE_EXPAND	(1<<0)
#define MODE_CONVERT	(1<<1)

    while ((ch = GETOPT(argc, argv, "cx")) > 0) {
	switch (ch) {
	case 'c':
	    mode |= MODE_CONVERT;
	    break;
	case 'v':
	    msg_verbose++;
	    break;
	case 'x':
	    mode |= MODE_EXPAND;
	    break;
	default:
	    usage(argv);
	}
    }
    switch (mode) {
    case MODE_CONVERT:
	test_convert();
	break;
    case MODE_EXPAND:
	test_expand();
	break;
    default:
	usage(argv);
    }
    exit(0);
}

#endif
