/*++
/* NAME
/*	known_tcp_ports 3
/* SUMMARY
/*	reduce dependency on the services(5) database
/* SYNOPSIS
/*	#include <known_tcp_ports.h>
/*
/*	const char *add_known_tcp_port(
/*	const char *name)
/*	const char *port)
/*
/*	const char *filter_known_tcp_port(
/*	const char *name_or_port)
/*
/*	void    clear_known_tcp_ports(void)
/* AUXILIARY FUNCTIONS
/*	char	*export_known_tcp_ports(
/*	VSTRING *result)
/* DESCRIPTION
/*	This module reduces dependency on the services(5) database.
/*
/*	add_known_tcp_port() associates a symbolic name with a numerical
/*	port. The function returns a pointer to error text if the
/*	arguments are malformed or if the symbolic name already has
/*	an association.
/*
/*	filter_known_tcp_port() returns the argument if it does not
/*	specify a symbolic name, or if the argument specifies a symbolic
/*	name that is not associated with a numerical port.  Otherwise,
/*	it returns the associated numerical port.
/*
/*	clear_known_tcp_ports() destroys all name-number associations.
/*	string.
/*
/*	export_known_tcp_ports() overwrites a VSTRING with all known
/*	name=port associations, sorted by service name, and separated
/*	by whitespace. The result is pointer to the VSTRING payload.
/* 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 <stdlib.h>
#include <string.h>

 /*
  * Utility library
  */
#include <htable.h>
#include <mymalloc.h>
#include <stringops.h>

 /*
  * Application-specific.
  */
#include <known_tcp_ports.h>

#define STR(x)	vstring_str(x)

static HTABLE *known_tcp_ports;

/* add_known_tcp_port - associate symbolic name with numerical port */

const char *add_known_tcp_port(const char *name, const char *port)
{
    if (alldig(name))
	return ("numerical service name");
    if (!alldig(port))
	return ("non-numerical service port");
    if (known_tcp_ports == 0)
	known_tcp_ports = htable_create(10);
    if (htable_locate(known_tcp_ports, name) != 0)
	return ("duplicate service name");
    (void) htable_enter(known_tcp_ports, name, mystrdup(port));
    return (0);
}

/* filter_known_tcp_port - replace argument if associated with known port */

const char *filter_known_tcp_port(const char *name_or_port)
{
    HTABLE_INFO *ht;

    if (name_or_port == 0 || known_tcp_ports == 0 || alldig(name_or_port)) {
	return (name_or_port);
    } else if ((ht = htable_locate(known_tcp_ports, name_or_port)) != 0) {
	return (ht->value);
    } else {
	return (name_or_port);
    }
}

/* clear_known_tcp_ports - destroy all name-port associations */

void    clear_known_tcp_ports(void)
{
    htable_free(known_tcp_ports, myfree);
    known_tcp_ports = 0;
}

/* compare_ht_keys - compare table keys */

static int compare_ht_keys(const void *a, const void *b)
{
    HTABLE_INFO **ap = (HTABLE_INFO **) a;
    HTABLE_INFO **bp = (HTABLE_INFO **) b;

    return (strcmp((const char *) ap[0]->key, (const char *) bp[0]->key));
}

/* export_known_tcp_ports - sorted dump */

char   *export_known_tcp_ports(VSTRING *out)
{
    HTABLE_INFO **list;
    HTABLE_INFO **ht;

    VSTRING_RESET(out);
    if (known_tcp_ports) {
	list = htable_list(known_tcp_ports);
	qsort((void *) list, known_tcp_ports->used, sizeof(*list),
	      compare_ht_keys);
	for (ht = list; *ht; ht++)
	    vstring_sprintf_append(out, "%s%s=%s", ht > list ? " " : "",
				   ht[0]->key, (const char *) ht[0]->value);
	myfree((void *) list);
    }
    VSTRING_TERMINATE(out);
    return (STR(out));
}

#ifdef TEST

#include <msg.h>

struct association {
    const char *lhs;			/* service name */
    const char *rhs;			/* service port */
};

struct probe {
    const char *query;			/* query */
    const char *exp_reply;		/* expected reply */
};

struct test_case {
    const char *label;			/* identifies test case */
    struct association associations[10];
    const char *exp_err;		/* expected error */
    const char *exp_export;		/* expected export output */
    struct probe probes[10];
};

struct test_case test_cases[] = {
    {"good",
	 /* association */ {{"smtp", "25"}, {"lmtp", "24"}, 0},
	 /* error */ 0,
	 /* export */ "lmtp=24 smtp=25",
	 /* probe */ {{"smtp", "25"}, {"1", "1"}, {"x", "x"}, {"lmtp", "24"}, 0}
    },
    {"duplicate lhs",
	 /* association */ {{"smtp", "25"}, {"smtp", "100"}, 0},
	 /* error */ "duplicate service name"
    },
    {"numerical lhs",
	 /* association */ {{"100", "100"}, 0},
	 /* error */ "numerical service name"
    },
    {"symbolic rhs",
	 /* association */ {{"smtp", "lmtp"}, 0},
	 /* error */ "non-numerical service port"
    },
    {"uninitialized",
	 /* association */ {0},
	 /* error */ 0,
	 /* export */ "",
	 /* probe */ {{"smtp", "smtp"}, {"1", "1"}, {"x", "x"}, 0}
    },
    0,
};

int     main(int argc, char **argv)
{
    VSTRING *export_buf;
    struct test_case *tp;
    struct association *ap;
    struct probe *pp;
    int     pass = 0;
    int     fail = 0;
    const char *err;
    int     test_failed;
    const char *reply;
    const char *export;

#define STRING_OR_NULL(s) ((s) ? (s) : "(null)")

    export_buf = vstring_alloc(100);
    for (tp = test_cases; tp->label != 0; tp++) {
	test_failed = 0;
	for (err = 0, ap = tp->associations; err == 0 && ap->lhs != 0; ap++)
	    err = add_known_tcp_port(ap->lhs, ap->rhs);
	if (!err != !tp->exp_err) {
	    msg_warn("test case %s: got error: \"%s\", want: \"%s\"",
	       tp->label, STRING_OR_NULL(err), STRING_OR_NULL(tp->exp_err));
	    test_failed = 1;
	} else if (err != 0) {
	    if (strcmp(err, tp->exp_err) != 0) {
		msg_warn("test case %s: got err: \"%s\", want: \"%s\"",
			 tp->label, err, tp->exp_err);
		test_failed = 1;
	    }
	} else {
	    export = export_known_tcp_ports(export_buf);
	    if (strcmp(export, tp->exp_export) != 0) {
		msg_warn("test case %s: got export: \"%s\", want: \"%s\"",
			 tp->label, export, tp->exp_export);
		test_failed = 1;
	    }
	    for (pp = tp->probes; test_failed == 0 && pp->query != 0; pp++) {
		reply = filter_known_tcp_port(pp->query);
		if (strcmp(reply, pp->exp_reply) != 0) {
		    msg_warn("test case %s: got reply: \"%s\", want: \"%s\"",
			     tp->label, reply, pp->exp_reply);
		    test_failed = 1;
		}
	    }
	}
	clear_known_tcp_ports();
	if (test_failed) {
	    msg_info("%s: FAIL", tp->label);
	    fail++;
	} else {
	    msg_info("%s: PASS", tp->label);
	    pass++;
	}
    }
    msg_info("PASS=%d FAIL=%d", pass, fail);
    vstring_free(export_buf);
    exit(fail != 0);
}

#endif
