/*
 * Copyright (c) 1995, 1996, 1997 Sun Microsystems, Inc.
 *
 * This software is provided by Sun ``AS IS'' and any express or implied
 * warranties, including, but not limited to, the implied warranties of
 * merchantability and fitness for a particular purpose are disclaimed.
 * In no event shall Sun Microsystems be liable for any direct, indirect,
 * incidental, special, exemplary, or consequential damages.
 *
 * This software is not a product, and is provided for evaluation purposes
 * only.
 *
 * This software may not be resold without the express permission of
 * Sun Microsystems.
 *
 * Feedback on this tool may be sent to packet-shell-owner@sunroof.eng.sun.com.
 *
 */

#pragma ident	"@(#)dhcp.c	1.22	99/05/28 SMI"

#include <sys/types.h>			/* keep this first */

#include <tcl.h>
#include <sys/socket.h>
#include <netinet/in.h>			/* for struct in_addr */
#include <arpa/inet.h>			/* inet_addr */
#include <stddef.h>			/* for ANSI C offsetof macro */
#include <netdb.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <ctype.h>

#include "psh.h"
#include "proto.h"

/*
 * XXX these are copies of the header files provided with the dhcp server.
 * unfortunately, that means they might get out of date.
 */

#include "dhcp.h"			/* for dhcp enums and structs */
#include "dhcp_util.h"			/* for _dhcp_options_scan()   */

/*
 *  IMPROVEMENT AREAS:
 *
 *  o  options which take multiple values, i.e. "raw input" currently use
 *     different arguments for each value.  they should instead probably
 *     separate based on spaces.
 *
 *  o  the `options' option field is not handled as well as it should be.
 *     in particular, it is taken to always be PKT_BUFFER -
 *     BASE_PKT_SIZE bytes, which is not correct in all cases.  however,
 *     it shouldn't segfault as a result
 *
 *  o  `p' suffix isn't used.  coding guidelines want it.
 *
 */

enum { HRD_ETHER_10MB = 1	};
enum { HRD_ETHER_10MB_LEN = 6 	};

#define	BUFFSIZE		128
#define	DHCP_LOWAT		300
#define	DHCP_LEN(pckt, ind)	(pckt->p_len - pckt->p_tbl[ind].pt_offset)
#define	N_ITEMS(x)		(sizeof (x)/ sizeof (x[0]))
#define	FROM_DATA(dst, src)	((void) memcpy(dst, src, sizeof (*dst)))
#define	TO_DATA(dst, src)	((void) memcpy(dst, src, sizeof (*src)))
#define	SIZE(s, f)		(sizeof (((s *)NULL)->f))
#define	END_OFFSET(s, f)	(offsetof(s, f) + SIZE(s, f) - 1)
#define	OFFSET_AND_SIZE(s, f)	offsetof(s, f),	 SIZE(s, f)	/* no parens */

#define	PKT_BUFFER	1486

/*
 * default packet types.  user can provide type when they use the
 * psh `pinit' command.	 for example, the user might say:
 *
 * psh(0): pinit dhcp dhcp_pkt OFFER
 *
 * to make an offer packet.  this initializes many of the fields to
 * have sane defaults, others of the fields to 0.  the user can
 * also specify the type by number, if desired.	 if no type is
 * given, the packet is initialized to all 0's.
 */

static struct dhcp dhcp_packet_types[] = {

	{ 0, /* all zeros */ },		   /* if user provides no type */

	/*
	 * format:
	 *
	 * op, hardware type, hardware addr len, hops, xid, secs,
	 * flags, ciaddr, yiaddr, siaddr, giaddr, chaddr,
	 * sname, file,
	 * options: magic, dhcp message type, end option
	 */

	/*
	 * XXX right now hardware type and len are hardcoded.  these
	 * should probably be made more dynamic in the future somehow
	 */

	/* DISCOVER */
	{ BOOTREQUEST,	HRD_ETHER_10MB, HRD_ETHER_10MB_LEN, 0, 0, 0,
	    BCAST_OFF,	 {0}, {0}, {0}, {0},	{ 0, /* chaddr 0 */	},
	    { 0, /* sname 0 */ },		{ 0, /* file 0 */	},
	    BOOTMAGIC, { CD_DHCP_TYPE, 1, DISCOVER, CD_END, /* 0 */	}
	},

	/* OFFER */
	{ BOOTREPLY,	HRD_ETHER_10MB, HRD_ETHER_10MB_LEN, 0, 0, 0,
	    BCAST_OFF,	 {0}, {0}, {0}, {0},	{ 0, /* chaddr 0 */	},
	    { 0, /* sname 0 */ },		{ 0, /* file 0 */	},
	    BOOTMAGIC, { CD_DHCP_TYPE, 1, OFFER, CD_END, /* 0 */	}
	},

	/* REQUEST */
	{ BOOTREQUEST,	HRD_ETHER_10MB, HRD_ETHER_10MB_LEN, 0, 0, 0,
	    BCAST_OFF,	 {0}, {0}, {0}, {0},	{ 0, /* chaddr 0 */	},
	    { 0, /* sname 0 */ },		{ 0, /* file 0 */	},
	    BOOTMAGIC, { CD_DHCP_TYPE, 1, REQUEST, CD_END, /* 0 */	}
	},

	/* DECLINE */
	{ BOOTREQUEST,	HRD_ETHER_10MB, HRD_ETHER_10MB_LEN, 0, 0, 0,
	    BCAST_OFF,	 {0}, {0}, {0}, {0},	{ 0, /* chaddr 0 */	},
	    { 0, /* sname 0 */ },		{ 0, /* file 0 */	},
	    BOOTMAGIC, { CD_DHCP_TYPE, 1, DECLINE, CD_END, /* 0 */	}
	},

	/* ACK */
	{ BOOTREPLY,	HRD_ETHER_10MB, HRD_ETHER_10MB_LEN, 0, 0, 0,
	    BCAST_OFF,	 {0}, {0}, {0}, {0},	{ 0, /* chaddr 0 */	},
	    { 0, /* sname 0 */ },		{ 0, /* file 0 */	},
	    BOOTMAGIC, { CD_DHCP_TYPE, 1, ACK, CD_END, /* 0 */		}
	},

	/* NAK */
	{ BOOTREPLY,	HRD_ETHER_10MB, HRD_ETHER_10MB_LEN, 0, 0, 0,
	    BCAST_OFF,	 {0}, {0}, {0}, {0},	{ 0, /* chaddr 0 */	},
	    { 0, /* sname 0 */ },		{ 0, /* file 0 */	},
	    BOOTMAGIC, { CD_DHCP_TYPE, 1, NAK, CD_END, /* 0 */		}
	},

	/* RELEASE */
	{ BOOTREQUEST,	HRD_ETHER_10MB, HRD_ETHER_10MB_LEN, 0, 0, 0,
	    BCAST_OFF,	 {0}, {0}, {0}, {0},	{ 0, /* chaddr 0 */	},
	    { 0, /* sname 0 */ },		{ 0, /* file 0 */	},
	    BOOTMAGIC, { CD_DHCP_TYPE, 1, RELEASE, CD_END, /* 0 */	}
	},

	/* INFORM */
	{ BOOTREQUEST,	HRD_ETHER_10MB, HRD_ETHER_10MB_LEN, 0, 0, 0,
	    BCAST_OFF,	 {0}, {0}, {0}, {0},	{ 0, /* chaddr 0 */	},
	    { 0, /* sname 0 */ },		{ 0, /* file 0 */	},
	    BOOTMAGIC, { CD_DHCP_TYPE, 1, INFORM, CD_END, /* 0 */	}
	}
};

/*
 * every dhcp field is included in the global `dhcp_fields[]' array,
 * and every dhcp option is included in the global `dhcp_opts[]'
 * array. these two objects are composed of arrays of type `struct
 * dhcp_field_info' and `struct dhcp_opt_info' respectively.  these
 * structures are similar - the similarities are described first.
 *
 * first, the `name' field should be a string containing a verbose
 * description of the field or option which the user can use to refer
 * to it in psh.  additionally, the `to_raw' and `from_raw' pointers
 * to functions are used to convert the data from a human-digestable
 * format into an internal representation, and back.  for example,
 * the user might want to set a field to his IP address, which he
 * enters in traditional dotted-decimal ascii.	it would then be the
 * job of `to_raw' to convert this ascii representation into the
 * network-byte-order long which actually gets stored in the `struct
 * dhcp *', and the job of `from_raw' to convert it back to a
 * human-readable string.
 *
 * `to_raw'   should return 1 (BTRUE) on success, 0 (BFALSE) otherwise.
 * `from_raw' does not return a value.
 *
 * there are a few pre-canned conversion routines to handle common cases:
 *
 * from_ip        from_ip_list      from_str	   from_bool	  from_uint
 * to_ip          to_ip_list        to_str	   to_bool	  to_uint
 *
 * to_keep_raw    to_u16_list       from_sint
 * from_keep_raw  from_u16_list     to_uint
 *
 * these, and other `to_raw' and `from_raw' conversion functions take
 * a `struct dhcp_field_info *' as a parameter.	 this structure is
 * guaranteed to contain fields with correct values.  of these fields,
 * `offset' contains the offset in the `struct dhcp' to get or set the
 * raw data.  additionally, `size' contains the number of bytes which
 * should be read or written from or to `offset'.
 *
 * in order for the `offset' and `size' parameters to be correct,
 * they must be set prior to calling the `to_raw' and `from_raw'
 * functions.  fields in `dhcp_fields[]' should use the macro
 * `OFFSET_AND_SIZE' to determine their offset and size at compile
 * time.
 *
 * for option fields, a `dhcp_field_info' structure will be
 * constructed at runtime by the option dispatcher, and the `offset'
 * and `size' fields will be computed, using the information in an
 * option's `dhcp_opt_info' structure, along with user input.
 *
 * the details of this conversion are fairly straightforward and also
 * not important, thus they are not described (UTSL).
 *
 * all options must be included in `dhcp_opts[]' when the module is
 * compiled.  as previously mentioned, each of these option fields is
 * of type `struct dhcp_opt_info'.  the fields unique to this
 * structure are:
 *
 *  `min_args' :  the minimum number of arguments this option accepts.
 *		  for most options, this is 1.
 *
 *  `arg_size' :  if `arg_size' is > 0, then it indicates the amount of
 *		  space each argument will take up in the `struct dhcp'.
 *		  for example, for an IP address, this would be 4, which
 *		  is the sizeof (struct in_addr).
 *
 *		  if `arg_size' is == 0, then the amount of space will
 *		  be computed at runtime using strlen()
 *
 *  `groups_of':  for options which take only one arg, this should be 0.
 *		  for options which take any number of args, this should
 *		  be 1.	 for options which take args in some multiple
 *		  (perhaps, sets of two), this should be that multiple.
 *
 * the ordering of the options in `dhcp_opts[]' *is* significant.
 * `dhcp_opts[]' is a 256-element array, of which each element refers
 * to a specific option.  for example, to provide support for dhcp
 * option 27, `dhcp_opts[27]' should contain a `struct dhcp_opt_info'
 * structure representing that option.
 */

struct	dhcp_field_info;		/* needed for typedefs */

typedef	void	from_func(
	    Tcl_Interp				*interp,
	    const struct dhcp_field_info	*field,
	    const union dhcp_un			*packet);

typedef	bool_t to_func(
	    const struct dhcp_field_info	*field,
	    union dhcp_un			*packet,
	    int					 argc,
	    char				*argv[]);

struct	dhcp_field_info {
	const char	*name;		/* name of the field */

	size_t		 offset;	/* offset into struct dhcp */
	size_t		 size;		/* size of the field */

	to_func		*to_raw;	/* conversion into raw form */
	from_func 	*from_raw;	/* conversion from raw form */
};

struct	dhcp_opt_info {
	const char	*name;		/* name of the option */

	size_t		 min_args;	/* minimum number of args to option */
	size_t		 arg_size;	/* the sizeof each arg passed to opt */
	size_t		 groups_of;	/* how the args have to be grouped */

	to_func		*to_raw;	/* conversion into raw form */
	from_func	*from_raw;	/* conversion from raw form */
};


static	from_func	from_ip_list,	from_u16_list,		from_ip,
			from_uint, 	from_dhcp_msg, 		from_keep_raw,
			from_str,	from_bool, 		from_opt_over,
			from_nb_tcpnt,	from_op,		from_chaddr,
			from_htype,	from_flags,		from_sint;

static	to_func		to_ip_list,	to_keep_raw,		to_op,
			to_uint,	to_dhcp_msg, 		to_nb_tcpnt,
			to_ip,		to_bool,		to_opt_over,
			to_u16_list,	to_str,			to_htype;

static const struct dhcp_field_info dhcp_fields[] = {
{ "op",		OFFSET_AND_SIZE(PKT, op),	to_op, 		from_op    },
{ "htype",	OFFSET_AND_SIZE(PKT, htype),	to_htype, 	from_htype },
{ "hlen", 	OFFSET_AND_SIZE(PKT, hlen),	to_uint, 	from_uint  },
{ "hops", 	OFFSET_AND_SIZE(PKT, hops),	to_uint, 	from_uint  },
{ "xid", 	OFFSET_AND_SIZE(PKT, xid),	to_uint, 	from_uint  },
{ "secs",	OFFSET_AND_SIZE(PKT, secs),	to_uint, 	from_uint  },
{ "flags", 	OFFSET_AND_SIZE(PKT, flags),	to_uint, 	from_flags },
{ "ciaddr", 	OFFSET_AND_SIZE(PKT, ciaddr),	to_ip, 		from_ip	   },
{ "yiaddr", 	OFFSET_AND_SIZE(PKT, yiaddr),	to_ip, 		from_ip	   },
{ "siaddr",	OFFSET_AND_SIZE(PKT, siaddr),	to_ip, 		from_ip	   },
{ "giaddr", 	OFFSET_AND_SIZE(PKT, giaddr),	to_ip, 		from_ip	   },
{ "chaddr", 	OFFSET_AND_SIZE(PKT, chaddr),	to_keep_raw, 	from_chaddr},
{ "sname",	OFFSET_AND_SIZE(PKT, sname),	to_str, 	from_str   },
{ "file",	OFFSET_AND_SIZE(PKT, file),	to_str,		from_str   },
};

/*
 * XXX add dynamic site options in the future?  vendor opts too?
 */

static const struct dhcp_opt_info dhcp_opts[256] = {
{  NULL,			0, 0, 0, NULL,		NULL		},
{ "subnet", 			1, 4, 0, to_ip,		from_ip		},
{ "time offset", 		1, 4, 0, to_uint, 	from_sint	},
{ "router", 			1, 4, 1, to_ip_list,	from_ip_list	},
{ "time server",		1, 4, 1, to_ip_list, 	from_ip_list	},
{ "name server",		1, 4, 1, to_ip_list,	from_ip_list	},
{ "domain name server",		1, 4, 1, to_ip_list,	from_ip_list	},
{ "log server",			1, 4, 1, to_ip_list,	from_ip_list	},
{ "cookie server",		1, 4, 1, to_ip_list,	from_ip_list	},
{ "lpr server",			1, 4, 1, to_ip_list,	from_ip_list	},
{ "impress server",		1, 4, 1, to_ip_list,	from_ip_list	},
{ "resource location server",	1, 4, 1, to_ip_list,	from_ip_list	},
{ "host name",			1, 0, 0, to_str,	from_str	},
{ "boot file size",		1, 2, 0, to_uint,	from_uint	},
{ "merit dump file",		1, 0, 0, to_str,	from_str	},
{ "domain name",		1, 0, 0, to_str,	from_str	},
{ "swap server",		1, 4, 0, to_ip,		from_ip		},
{ "root path",			1, 0, 0, to_str,	from_str	},
{ "extensions path",		1, 0, 0, to_str,	from_str	},
{ "ip forwarding",		1, 1, 0, to_bool,	from_bool	},
{ "non-local source routing",	1, 1, 0, to_bool,	from_bool	},
{ "policy filter",		2, 4, 2, to_ip_list,	from_ip_list	},
{ "max datagram reassembly",	1, 2, 0, to_uint,	from_uint	},
{ "default ip ttl",		1, 1, 0, to_uint,	from_uint	},
{ "path aging mtu timeout", 	1, 4, 0, to_uint,	from_uint	},
{ "path mtu plateau table",	2, 2, 2, to_u16_list,	from_u16_list	},
{ "interface mtu",		1, 2, 0, to_uint,	from_uint	},
{ "all subnets are local", 	1, 1, 0, to_bool, 	from_bool	},
{ "broadcast address",		1, 4, 0, to_ip, 	from_ip		},
{ "perform mask discovery",	1, 1, 0, to_bool,	from_bool	},
{ "mask supplier",		1, 1, 0, to_bool,	from_bool	},
{ "perform router discovery",	1, 1, 0, to_bool,	from_bool	},
{ "router solicitation addr",	1, 4, 0, to_ip,		from_ip		},
{ "static route",		2, 4, 2, to_ip_list,	from_ip_list	},
{ "trailer encapsulation",	1, 1, 0, to_bool,	from_bool	},
{ "arp cache timeout",		1, 4, 0, to_uint,	from_uint	},
{ "ethernet encapsulation",	1, 1, 0, to_bool,	from_bool	},
{ "tcp default ttl",		1, 1, 0, to_uint,	from_uint	},
{ "tcp keepalive interval",	1, 4, 0, to_uint,	from_uint	},
{ "tcp keepalive garbage",	1, 1, 0, to_bool,	from_bool	},
{ "nis domain",			1, 0, 0, to_str, 	from_str	},
{ "nis servers",		1, 4, 1, to_ip_list,	from_ip_list	},
{ "ntp servers",		1, 4, 1, to_ip_list,	from_ip_list	},
{ "vendor specific info",	1, 1, 1, to_keep_raw,	from_keep_raw	},
{ "netbios-tcp/ip name server",	1, 4, 1, to_ip_list, 	from_ip_list	},
{ "netbios-tcp/ip datagram dist server",
				1, 4, 1, to_ip_list,	from_ip_list	},
{ "netbios-tcp/ip node type",	1, 1, 0, to_nb_tcpnt,	from_nb_tcpnt	},
{ "netbios-tcp/ip scope",	1, 0, 0, to_str, 	from_str	},
{ "x windows font server", 	1, 4, 1, to_ip_list, 	from_ip_list	},
{ "x windows display manager",	1, 4, 1, to_ip_list, 	from_ip_list	},
{ "requested ip address",	1, 4, 0, to_ip, 	from_ip		},
{ "ip address lease time", 	1, 4, 0, to_uint, 	from_uint	},
{ "option overload",		1, 1, 0, to_opt_over, 	from_opt_over	},
{ "dhcp message type",		1, 1, 0, to_dhcp_msg, 	from_dhcp_msg	},
{ "server identifier",		1, 4, 0, to_ip, 	from_ip		},
{ "parameter request list", 	1, 1, 1, to_keep_raw, 	from_keep_raw	},
{ "message",			1, 0, 0, to_str, 	from_str	},
{ "max dhcp message size", 	1, 2, 0, to_uint,	from_uint	},
{ "t1 renewal time value", 	1, 4, 0, to_uint, 	from_uint	},
{ "t2 rebinding time value", 	1, 4, 0, to_uint, 	from_uint	},
{ "vendor class identifier", 	1, 0, 0, to_str, 	from_str	},
{ "client identifier",		2, 1, 1, to_keep_raw,	from_keep_raw	},
{ "netware ip domain",		1, 0, 0, to_str, 	from_str	},
{ "netware ip options", 	1, 1, 1, to_keep_raw, 	from_keep_raw	},
{ "nis+ domain",		1, 0, 0, to_str, 	from_str 	},
{ "nis+ servers", 		1, 4, 1, to_ip_list,	from_ip_list	},
{ "tftp server",		1, 0, 0, to_str, 	from_str	},
{ "bootfile name",		1, 0, 0, to_str, 	from_str	},
{ "home agents",		0, 4, 1, to_ip_list,	from_ip_list	},
{ "smtp server",		1, 4, 1, to_ip_list,	from_ip_list	},
{ "pop server",			1, 4, 1, to_ip_list,	from_ip_list	},
{ "nntp server",		1, 4, 1, to_ip_list,	from_ip_list	},
{ "www server",			1, 4, 1, to_ip_list,	from_ip_list	},
{ "finger server",		1, 4, 1, to_ip_list,	from_ip_list	},
{ "irc server", 		1, 4, 1, to_ip_list,	from_ip_list	},
{ "streettalk server",		1, 4, 1, to_ip_list,	from_ip_list	},
{ "stda server", 		1, 4, 1, to_ip_list,	from_ip_list	},
};

/*
 * we will need to know a bunch of information about the options fields
 * (i.e. `options' itself, plus the potentially overloaded `sname' and
 * `file' fields.) -- let's get it defined up front.
 */

static const struct opt_field_info {
	size_t	  offset;	/* starting offset */
	size_t	  size;		/* size of field   */
	size_t	  off_end;	/* ending offset   */
	char	  bit_lit;	/* bit lit in OO opt if field is overloaded */
} opt_fields[] = {

	/*
	 * this first entry is slightly bogus, since the first option field
	 * (`options') is actually variable length, so the `size' and
	 * `off_end' structure fields don't really apply.  however, since
	 * we assume in this module that the DHCP part of a packet will
	 * at most be PKT_BUFFER bytes, we set the `size' and
	 * `off_end' to these maximums.  in practice, this isn't really a
	 * problem, since we always make sure the packet is this big before
	 * calling any routines that use this information.
	 */

	{ offsetof(PKT, options),  PKT_BUFFER - BASE_PKT_SIZE,
	    offsetof(PKT, options) + PKT_BUFFER - BASE_PKT_SIZE - 1, },
	{ OFFSET_AND_SIZE(PKT, sname),	END_OFFSET(PKT, sname),	0x02 },
	{ OFFSET_AND_SIZE(PKT, file),	END_OFFSET(PKT, file),	0x01 }
};

/*
 * these are indexes which correlate `opt_fields[]' entries with which
 * options field they describe.
 */

enum { DHCP_OPT_OPT, DHCP_OPT_SNAME, DHCP_OPT_FILE };

/*
 * these arrays are used by the from_enum() and to_enum() routines below.
 *
 * for each array of strings, the index of the string in the array is
 * equal to the number associated with it in the DHCP/BOOTP spec.  the last
 * element should always be a descriptive phrase used when the enum()
 * routines cannot resolve the int->string or string->int conversion.
 */

static const char *dhcp_msg_types[] = {
	"raw",			"discover",		"offer",
	"request",		"decline",		"ack",
	"nak",			"release",		"inform",
	"unknown dhcp msg type"		/* keep last */
};

/*
 * hardware types from ftp.isi.edu:/in-notes/iana/assignments/arp-parameters,
 * as referenced by RFC 1700.
 */

static const char *htypes[] = {
	NULL,			"10 mb ethernet",	"3 mb expr ethernet",
	"amateur radio",	"proteon pronet token ring", "chaos",
	"ieee 802 networks",	"arcnet",		"hyperchannel",
	"lanstar",		"autonet short access", "localtalk",
	"localnet",		"ultra link", 		"smds",
	"frame relay",		"atm",			"hdlc",
	"fibre channel",	"atm",			"serial line",
	"atm",			"mil-std-188-220",	"metricom",
	"ieee 1394.1995",	"mapos",		"twinaxial",
	"unknown hardware type"		/* keep last */
};

static const char *ops[] = {		/* from RFC 951 */
	NULL,			"BOOTREQUEST",		"BOOTREPLY",
	"unknown op"			/* keep last */
};

static const char *overloads[] = {
	NULL,			"file overloaded",	"sname overloaded",
	"sname and file overloaded",
	"unknown overload"		/* keep last */
};

static const char *netbios_node_types[] = {
	NULL,			"B-node",		"P-node",
	NULL, 			"M-node",		NULL,
	NULL,			"H-node",
	"unknown node type"		/* keep last */
};

/* we don't know the option names until runtime, so just set to NULL for now */
static const char **dhcp_opt_names = NULL;
#define	N_DHCP_OPT_NAMES	(N_ITEMS(dhcp_opts) + 1)

static char	   *proto_name;

#ifndef	NO_SNPRINTF
/*
 * We have snprintf(), so let's just use it.
 */
#define	__snprintf	snprintf
#else
/*
 * For silly compatibility reasons we have this function.  It's implemented
 * on "solid ground"--it works on everything.  On Solaris 2.6 and later, and
 * some other systems, "snprintf(...)" is good enough.
 */
#include <stdarg.h>
int
__snprintf(char *s, size_t n, const char *fmt, ...)
{
	int	retval;
	va_list ap;

	va_start(ap, fmt);
	retval = vsprintf(s, fmt, ap);
	va_end(ap);
	return (retval);
}
#endif

/*
 * from_enum()
 *
 * purpose: to convert a number into a string that it represents
 *   input: Tcl_Interp *  : the current tcl interpreter
 *	    size_t	  : the `number' to convert
 *	    const char *[]: the array of strings the number might convert to
 *	    size_t	  : the number of elements in the array (1-based)
 *  output: void
 */

static void
from_enum(Tcl_Interp *interp, size_t item, const char *names[], size_t n_items)
{
	static char tmp[BUFFSIZE];

	/*
	 * if the item has a description, use it, otherwise, return the index
	 * of the `unknown' description.
	 */

	size_t	name_ind = (item < n_items && names[item]) ? item : n_items - 1;

	(void) __snprintf(tmp, sizeof (tmp), "0x%x (%s)", item,
	    names[name_ind]);
	Tcl_AppendResult(interp, tmp, NULL);
}

/*
 * to_enum()
 *
 * purpose: to convert a string its numeric equivalent
 *   input: const char *  : the string to convert
 *	    const char *[]: the array of strings it might be, each index
 *			    being the value associated with the string
 *	    size_t	  : the number of items in the array (1-based)
 *  output: size_t: the numeric form
 */

static size_t
to_enum(const char *name, const char *names[], size_t n_items)
{
	size_t	   i;

	/* if the user entered a numeric value, just return it */
	if (isdigit(*name))
		return (strtoul(name, NULL, 0));

	/*
	 * go through each name in the table, and for each defined
	 * table entry, compare it with our input.
	 */
	for (i = 0; i < n_items; i++)
		if (names[i] && strncasecmp(names[i], name, strlen(name)) == 0)
			return (i);

	return (n_items - 1);		/* no match, return "unknown" element */
}

/*
 * init()
 *
 * purpose: to initialize the dhcp module
 *   input: proto_tbl_t *: the protocol table of this module, passed from psh
 *	    Tcl_Interp *: the current tcl interpreter
 *  output: int: nonzero on success, 0 on failure
 */

int
init(proto_tbl_t *prototbl, Tcl_Interp *interp)
{
	size_t	i;

	if ((proto_name = strdup(prototbl->proto_name)) == NULL) {
		Tcl_AppendResult(interp, "Internal Error: malloc: no space",
		    NULL);
		return (0);
	}
	(void) Tcl_SetVar(interp, "pset-protocols", proto_name,
	    TCL_LIST_ELEMENT|TCL_APPEND_VALUE);
	(void) Tcl_SetVar(interp, "pget-protocols", proto_name,
	    TCL_LIST_ELEMENT|TCL_APPEND_VALUE);

	/* set up the table of option names */
	dhcp_opt_names = calloc(N_DHCP_OPT_NAMES, sizeof (char *));
	if (dhcp_opt_names == NULL) {
		Tcl_AppendResult(interp, "Internal Error: malloc: no space",
		    NULL);
		return (0);
	}
	for (i = 0; i <= DHCP_LAST_OPT; i++)
		dhcp_opt_names[i] = dhcp_opts[i].name;

	dhcp_opt_names[i] = "unknown option";
	return (1);
}

/*
 * pinit()
 *
 * purpose: to create a `default' packet
 *   input: ClientData	   : <not used>
 *	    Tcl_Interp *   : the current tcl interpreter
 *	    struct packet *: the packet to allocate a buffer for
 *	    int		   : the argument count from `pinit' psh command
 *	    char *[]	   : the argument vector
 *  output: int: nonzero on success, 0 on failure
 */

int
pinit(ClientData clientData, Tcl_Interp *interp, struct packet *pckt,
    int argc, char *argv[])
{
	int		 type	= 0;		/* dhcp message type = raw */
	size_t		 i;


	if (argc < 3 || argc > 4) {
		Tcl_AppendResult(interp, "Usage: pinit ", proto_name,
		    " <pkt> <max. length> <structure length>",
		    " [<DHCP message type>]", NULL);
		return (TCL_ERROR);
	}

	if (argc == 4)			/* user provided their own msg type */
		type = to_enum(argv[3], dhcp_msg_types,
		    N_ITEMS(dhcp_msg_types));

	if (type == N_ITEMS(dhcp_msg_types) - 1) {

		Tcl_AppendResult(interp, proto_name, " pinit: DHCP ",
		    "message type is one of:\n", NULL);

		for (i = 0; i < N_ITEMS(dhcp_msg_types) - 1; i++)
			Tcl_AppendResult(interp, dhcp_msg_types[i], " ", NULL);
		return (TCL_ERROR);
	}

	if (pckt->p_len == 0)
		pckt->p_len = DHCP_LOWAT;

	/* copy in the right default packet */
	(void) memcpy(pckt->p_buf, &dhcp_packet_types[type], min(pckt->p_len,
	    sizeof (struct dhcp)));

	if (ptbl_update(pckt, 1, NULL, pckt->p_len) == 0) {
		return (TCL_ERROR);
	}
	return (TCL_OK);
}

/*
 * decode()
 *
 * purpose: to demux up to the next level in the protocol stack
 *   input: struct packet *: the packet to demux
 *	    int		   : <not used>
 *	    char **	   : pointer to string used to name next level
 *	    int *	   : pointer to int used to set next offset
 *  output: int: nonzero on success, 0 on failure
 */

int
decode(struct packet *pckt, int element, char **name, int *next_offset)
{
	/* as far as we know, there's no one encapsulating us */
	*name		= "end";
	*next_offset	= pckt->p_len;
	return (0);
}

/*
 * propen()
 *
 * purpose: to open an endpoint
 *   input: <not used>
 *  output: int: TCL_OK
 *   notes: just a facade.  DHCP actually uses the psh socket library
 */

int
propen(ClientData clientData, Tcl_Interp *interp, int argc, char *argv[])
{
	return (TCL_OK);		/* NOP */
}

/*
 * prclose()
 *
 * purpose: to close an endpoint
 *   input: <not used>
 *  output: int: TCL_OK
 *   notes: just a facade.  DHCP actually uses the psh socket library
 */

/* XXX add tcl commands for doing open, close */

int
prclose(ClientData clientData, Tcl_Interp *interp, int argc, char *argv[])
{
	return (TCL_OK);		/* NOP */
}

/*
 * get_opts()
 *
 * purpose: to find out what options are set in a DHCP packet
 *   input: union dhcp_un *   : the dhcp packet, in raw and struct dhcp form
 *	    struct dhcp_list *: a structure to store option info in
 *	    size_t	      : the size of the dhcp packet
 *  output: void
 */

static void
get_opts(union dhcp_un *dpckt, struct dhcp_list *dhcp_pkt, size_t pkt_sz)
{
	(void) memset(dhcp_pkt, 0, sizeof (*dhcp_pkt)); /* clear the packet */
	dhcp_pkt->pkt = &dpckt->dhcp;		 /* now init its fields */
	dhcp_pkt->len = pkt_sz;

	(void) _dhcp_options_scan(dhcp_pkt);
}

/*
 * make_field_info()
 *
 * purpose: to init a `struct dhcp_field_info', for an option using other info
 *   input: struct dhcp_field_info *	: the structure to initialize
 *	    const struct dhcp_opt_info *: option info for this option
 *	    DHCP_OPT *			: info about this option in the packet
 *	    const union dhcp_un *	: the dhcp packet, in raw and dhcp form
 *  output: void
 *   notes: `opt_scan' and `dpckt' may be NULL if not yet known.  in this
 *	     case, `offset' and `size' are set to 0.
 */

static void
make_field_info(struct dhcp_field_info *field, const struct dhcp_opt_info *opt,
    DHCP_OPT *opt_scan, const union dhcp_un *dpckt, struct packet *pckt)
{
	field->name	= opt->name;
	field->size	= opt_scan ? opt_scan->len : 0;
	field->offset	= opt_scan ? (caddr_t)opt_scan->value - dpckt->raw : 0;
	field->from_raw = opt->from_raw;
	field->to_raw	= opt->to_raw;

	/* make sure field->size isn't outrageous */
	if (pckt && field->offset + field->size > pckt->p_maxlen)
		field->size = pckt->p_maxlen - field->offset;
}

/*
 * get()
 *
 * purpose: to get the value of fields or options in the packet
 *   input: int		   : index of dhcp's layer in the stack for this packet
 *	    ClientData	   : <not used>
 *	    Tcl_Interp *   : the current tcl interpreter
 *	    struct packet *: the current packet, including the dhcp part
 *	    int		   : the argument count from psh `pget'
 *	    char *[]	   : the argument vector from psh `pget'
 *  output: int: TCL_OK on success, TCL_ERROR on failure
 */

int
get(int el, ClientData clientData, Tcl_Interp *interp, struct packet *pckt,
    int argc, char *argv[])
{
	size_t		 i;
	union dhcp_un	*dpckt =
	    (union dhcp_un *)(pckt->p_buf + pckt->p_tbl[el].pt_offset);

	if (argc < 1) {
		Tcl_AppendResult(interp, "Usage: pget ", pckt->p_name, " ",
		    proto_name, " <field>", NULL);
		return (TCL_ERROR);
	}

	/* search for the field we want, then call its from_raw func */
	for (i = 0; i < N_ITEMS(dhcp_fields); i++)
		if (strncasecmp(argv[0], dhcp_fields[i].name, strlen(argv[0]))
		    == 0) {
			dhcp_fields[i].from_raw(interp, &dhcp_fields[i], dpckt);
			return (TCL_OK);
		}

	/* see if we have to handle options */
	if (strncasecmp(argv[0], "options", strlen(argv[0])) == 0) {

		size_t			opt;
		struct dhcp_field_info	field_info;
		struct dhcp_list	dhcp_pkt;

		get_opts(dpckt, &dhcp_pkt, DHCP_LEN(pckt, el));

		if (argc == 1) {
			Tcl_AppendResult(interp, "Usage: pget ", pckt->p_name,
			    " ", proto_name, " option <field>", NULL);
			return (TCL_ERROR);
		}

		/* yep.	 call to_enum() to figure out what option user wanted */
		opt = to_enum(argv[1], dhcp_opt_names, N_DHCP_OPT_NAMES);

		if (opt >= DHCP_LAST_OPT + 1) {	  /* conversion failed */
			Tcl_AppendResult(interp, "pget: unrecognized option \"",
			    argv[1], "\"", NULL);
			return (TCL_ERROR);
		}

		if (dhcp_pkt.opts[opt] == NULL) { /* it's not set - return "" */
			Tcl_AppendResult(interp, "", NULL);
			return (TCL_OK);
		}

		make_field_info(&field_info, &dhcp_opts[opt],
		    dhcp_pkt.opts[opt], dpckt, pckt);

		if (field_info.from_raw)
			field_info.from_raw(interp, &field_info, dpckt);
		else
			from_keep_raw(interp, &field_info, dpckt);

		return (TCL_OK);
	}

	/* field wasn't found. show user the fields */
	Tcl_AppendResult(interp, "pget: <field> is one of:\n", NULL);
	for (i = 0; i < N_ITEMS(dhcp_fields); i++)
		Tcl_AppendResult(interp, dhcp_fields[i].name, " ", NULL);

	return (TCL_ERROR);
}

/*
 * get_opt_end()
 *
 * purpose: to find the current end of an `options' field
 *   input: caddr_t: pointer to start of an `options' field
 *	    size_t : the size of the options field
 *  output: caddr_t: pointer to first free byte in the options field,
 *		     or one off the end if no space free.  a `CD_END'
 *		     option is considered a free byte.
 */

static caddr_t
get_opt_end(caddr_t first_opt, size_t field_size)
{
	caddr_t cur_opt	 = first_opt;

	/*
	 * if the first character of an options field is CD_PAD, we
	 * stop scanning, since RFC 2131 explicitly prohibits this
	 */

	if (((DHCP_OPT *)cur_opt)->code == CD_PAD)
		return (cur_opt);

	while (cur_opt - first_opt < field_size) {

		switch (((DHCP_OPT *)cur_opt)->code) {
		case CD_END:
			return (cur_opt);
		case CD_PAD:
			cur_opt++;
			break;
		default:
			cur_opt += ((DHCP_OPT *)cur_opt)->len + 2;
		}
	}

	/*
	 * hmm. there wasn't a CD_END.	look for first CD_PAD, or
	 * return character after end of field if the options field
	 * is completely full.
	 */

	for (cur_opt = first_opt + field_size - 1; *cur_opt == CD_PAD; )
		cur_opt--;

	return (cur_opt + 1);
}

/*
 * get_opt_end2()
 *
 * purpose: to find the current end of an `options' field
 *   input: caddr_t: pointer to start of an `options' field
 *	    size_t : the size of the options field
 *  output: caddr_t: pointer to last used byte in the options field.
 *		     `CD_END' is considered a used byte.
 */

static caddr_t
get_opt_end2(caddr_t first_opt, size_t field_size)
{
	caddr_t cur_end = get_opt_end(first_opt, field_size);

	if (cur_end >= first_opt + field_size)
		cur_end = first_opt + field_size - 1;

	return (((uchar_t)*cur_end == CD_END) ? cur_end : cur_end--);
}  

/*
 * get_opt_offset()
 *
 * purpose: to get the offset to store an option at in a packet
 *	    that does not currently have that option
 *   input: struct dhcp_field_info *: a place to store the offset
 *	    union dhcp_un *	    : packet to search
 *	    uchar_t overload	    : option overload status
 *  output: int: nonzero on success, 0 on failure
 */

static int
get_opt_offset(struct dhcp_field_info *opt_info, union dhcp_un *dpckt,
    uchar_t overload)
{
	caddr_t	end;			/* pointer to last byte of field */
	caddr_t	begin;			/* pointer to first byte of field */
	size_t	i;

	for (i = 0; i < N_ITEMS(opt_fields); i++) {
		if (i != DHCP_OPT_OPT &&
		    (overload & opt_fields[i].bit_lit) == 0)
			continue;

		begin	= dpckt->raw + opt_fields[i].offset;
		end	= get_opt_end(begin, opt_fields[i].size);

		/* if there's enough room, we're done */
		if (begin + opt_fields[i].size - end >= opt_info->size) {
			opt_info->offset = end - dpckt->raw;
			return (1);
		}
	}
	return (0);			/* no space available -- fail */
}

/*
 * shift_mem()
 *
 * purpose: to shift memory and zero out any left over pieces
 *   input: caddr_t: start of new place to put data
 *	    caddr_t: end of new place to put data
 *	    caddr_t: start of current place data is
 *	    caddr_t: end of current place data is
 *  output: void
 */

static void
shift_mem(caddr_t new_start, caddr_t new_end, caddr_t start, caddr_t end)
{
	size_t bytes_moved = end - start + 1;

	(void) memmove(new_start, start, bytes_moved);	/* MUST be memmove() */
	(void) memset(new_start + bytes_moved, 0,
	    new_end - (new_start + bytes_moved) + 1);
}

/*
 * find_option_field()
 *
 * purpose: to find the option field an option came from
 *   input: caddr_t		 : a pointer to the option in the packet
 *	    const union dhcp_un *: the packet itself
 *  output: size_t: the index
 */

static size_t
find_option_field(caddr_t cur_opt, const union dhcp_un *dpckt)
{
	size_t	opt_offset = cur_opt - dpckt->raw;
	size_t	i;

	/* find which option field this option came from */
	for (i = 0; i < N_ITEMS(opt_fields); i++)
		if (opt_offset >= opt_fields[i].offset &&
		    opt_offset <= opt_fields[i].off_end)
			break;		/* match */

	return (i);
}

/*
 * resize_opt()
 *
 * purpose: to resize an options field in a packet
 *   input: Tcl_Interp *   : the current tcl interpreter
 *	    union dhcp_un *: the packet whose options need resizing
 *	    caddr_t	   : the option to resize
 *	    int		   : the number of the option being resized
 *	    size_t	   : the new length of the option
 *	    struct packet *: the psh packet, with len info
 *  output: int: TCL_OK on success, or TCL_ERROR on failure
 */

static int
resize_opt(Tcl_Interp *interp, union dhcp_un *dpckt, caddr_t cur_opt,
    int opt, size_t opt_size, struct packet *pckt)
{
	/*
	 * this is admittedly gruesome. however, it is performing a complex
	 * task and i have to bite the bullet some time.
	 *
	 * `opt_field'	is the index of the option field this option is in
	 * `end_offset' is the offset of the last location in that options field
	 * `first_opt'	is the first option of that options field
	 * `cur_opt'	is the current option we're resizing
	 * `next_opt'	is the next option after it
	 * `space_free' is the amount of free space in that options field
	 * `opt_end'	is the last important byte in that options field
	 */

	size_t	opt_field  = find_option_field(cur_opt, dpckt);
	size_t	end_offset = opt_fields[opt_field].off_end;
	caddr_t first_opt  = (caddr_t)dpckt->raw + opt_fields[opt_field].offset;
	caddr_t next_opt   = cur_opt + ((DHCP_OPT *)cur_opt)->len + 2;
	size_t	space_free = next_opt - cur_opt;
	caddr_t opt_end    =
	    get_opt_end2(first_opt, opt_fields[opt_field].size);

	/* if it's the option overload option, fail */
	if (opt == CD_OPTION_OVERLOAD) {
		Tcl_AppendResult(interp, "pset: option overload cannot be ",
		    "changed", NULL);
		return (TCL_ERROR);
	}

	space_free += dpckt->raw + end_offset - opt_end + 1;

	/* is there enough space? */
	if (space_free < opt_size) {
		Tcl_AppendResult(interp, "pset: not enough room "
		    "left in options field", NULL);
		return (TCL_ERROR);
	}

	/*
	 * if we're making changes to the `options' options field, recompute
	 * the length of the packet
	 */

	if (opt_field == DHCP_OPT_OPT) {
		pckt->p_len = opt_end - dpckt->raw + opt_size -
		    (next_opt - cur_opt) + 1;
		if (pckt->p_len < DHCP_LOWAT)
			pckt->p_len = DHCP_LOWAT;
	}

	/*
	 * THIS opt_end is guaranteed to point to the last valid piece
	 * of information in the packet.  the rest of the remaining
	 * space should then be memset() to 0.	shift_mem will do this.
	 */

	shift_mem(cur_opt + opt_size, dpckt->raw + end_offset,
	    next_opt, opt_end);

	return (TCL_OK);
}

/*
 * set()
 *
 * purpose: to set or remove the values of fields or options in the packet
 *   input: int		   : index of dhcp's layer in the stack for this packet
 *	    ClientData	   : <not used>
 *	    Tcl_Interp *   : the current tcl interpreter
 *	    struct packet *: the current packet, including the dhcp part
 *	    int		   : the argument count from psh `pset'
 *	    char *[]	   : the argument vector from psh `pset'
 *  output: int: TCL_OK on success, TCL_ERROR on failure
 */

int
set(int el, ClientData clientData, Tcl_Interp *interp, struct packet *pckt,
    int argc, char *argv[])
{
	size_t		i;
	union dhcp_un  *dpckt =
	    (union dhcp_un *)(pckt->p_buf + pckt->p_tbl[el].pt_offset);

	if (argc < 2) {
		Tcl_AppendResult(interp, "Usage: pset ", pckt->p_name,
		    " ", proto_name, " <field> <value>", NULL);
		return (TCL_ERROR);
	}

	/* search for the field we want, then call its to_raw func */
	for (i = 0; i < N_ITEMS(dhcp_fields); i++)
		if (strncasecmp(argv[0], dhcp_fields[i].name,
				strlen(argv[0])) == 0) {
			return (dhcp_fields[i].to_raw(&dhcp_fields[i], dpckt,
			    argc, argv) ? TCL_OK : TCL_ERROR);
		}

	/* unless user specified an option, we're done */
	if (strncasecmp(argv[0], "options", strlen(argv[0])) != 0) {

		/* field wasn't found. show user the fields */
		Tcl_AppendResult(interp, "pset: <field> is one of:\n", NULL);
		for (i = 0; i < N_ITEMS(dhcp_fields); i++)
			Tcl_AppendResult(interp, dhcp_fields[i].name, " ",
			    NULL);

		return (TCL_ERROR);

	} else {			/* ugh - handle options	 */

		bool_t		is_del	= BFALSE;
		size_t			opt;
		struct dhcp_field_info	field_info;
		struct dhcp_list	dhcp_pkt;

		get_opts(dpckt, &dhcp_pkt, DHCP_LEN(pckt, el));

		if (argc < 2) {
			Tcl_AppendResult(interp, "Usage: pset ", pckt->p_name,
			    " ", proto_name, " option [del] <field> <value>",
			    NULL);
			return (TCL_ERROR);
		}

		/* handle the "del" option first */
		if (strcasecmp(argv[1], "del") == 0) {
			if (argc < 3) {
				Tcl_AppendResult(interp, "Usage: pset ",
				    pckt->p_name, " ", proto_name,
				    " option del <field>", NULL);
				return (TCL_ERROR);
			}

			is_del = BTRUE;
			argc--;		/* skip over "del" arg */
			argv++;
		}

		/*
		 * if the field is not already present, we want to add it to
		 * the end of the options.  there may be room in the options
		 * area, but no area left in the packet.  here, we grow the
		 * options field to the maximum size before possible
		 * fragmentation (PKT_BUFFER - BASE_PKT_SIZE bytes).
		 *
		 * if the options field cannot be grown any more, then we check
		 * to see if the `option overload' flag is currently in the
		 * option list.	 if it is present, the option will be placed
		 * in the next available slot in an overloaded field of the
		 * DHCP packet.	  if there is no room in any of these fields,
		 * or if the `option overload' flag is not specified, we fail.
		 *
		 * if the option is already present, then the change in size
		 * between the option's existing value and new value is
		 * taken.  if the change in space is smaller than the amount
		 * of space left in the field, then the options following
		 * the option being updated are shifted either right or left
		 * as necessary.  if there isn't space in the field, no attempt
		 * at compaction is attempted: it fails.  we could do better,
		 * but at the risk of excessive complexity.
		 *
		 * the value of the `option overload' field cannot be changed,
		 * once set.  if we allowed this, we'd be on our way to writing
		 * a memory allocator, which is getting just too insane.
		 */

		opt = to_enum(argv[1], dhcp_opt_names, N_DHCP_OPT_NAMES);

		if (opt > DHCP_LAST_OPT || dhcp_opts[opt].to_raw == NULL) {
			Tcl_AppendResult(interp, "pset: unrecognized option \"",
			    argv[1], "\"", NULL);
			return (TCL_ERROR);
		}

		if (is_del == BTRUE) {		/* handle delete stuff */

			caddr_t cur_opt = (caddr_t)dhcp_pkt.opts[opt];

			/* if option not present, print diagnostic */
			if (cur_opt == NULL) {
				Tcl_AppendResult(interp, "pset: option not "
				    "present", NULL);
				return (TCL_ERROR);
			}

			return (resize_opt(interp, dpckt, cur_opt, opt, 0,
			    pckt));
		}

		/*
		 * if the options field is not already as big as possible, then
		 * realloc().  note that a packet's size is determined by its
		 * maxlen.
		 */

		if (pckt->p_maxlen - pckt->p_tbl[el].pt_offset < PKT_BUFFER) {

			void   *new_pckt_buf;
			size_t	new_size = PKT_BUFFER +
			    pckt->p_tbl[el].pt_offset;
#ifdef DEBUG
			(void) fprintf(stderr,
			    "DEBUG: DHCP part of packet was only %i"
			    ", realloc()ing\n", pckt->p_maxlen -
			    pckt->p_tbl[el].pt_offset);
#endif
			new_pckt_buf = realloc(pckt->p_buf, new_size);
			if (new_pckt_buf == NULL) {
				Tcl_AppendResult(interp, "pset: couldn't grow "
				    "options field -- eek!", NULL);
				return (TCL_ERROR);
			}

			pckt->p_buf    = new_pckt_buf;
			pckt->p_maxlen = new_size;
		}

		argc--;			/* advance to the start of the values */
		argv++;

		/*
		 * as usual, we need to pass a `struct dhcp_field_info *' to
		 * the to_raw() functions.  we have most of the necessary
		 * info sitting around in `dhcp_opts[]' already -- call
		 * make_field_info to fill most of it in.
		 */

		make_field_info(&field_info, &dhcp_opts[opt], NULL, NULL, NULL);

		/*
		 * we need to make sure we have enough args, compute
		 * the total size of those args, and make sure we have
		 * args in the proper groupings.
		 */

		if (argc - 1 < dhcp_opts[opt].min_args) {
			Tcl_AppendResult(interp, "pset: not enough arguments "
			    "to option", NULL);
			return (TCL_ERROR);
		}

		if (dhcp_opts[opt].groups_of == 0) {
			if (argc - 1 != dhcp_opts[opt].min_args) {
				Tcl_AppendResult(interp, "pset: too many "
				    "arguments to option", NULL);
				return (TCL_ERROR);
			}
		} else {
			if ((argc - 1) % dhcp_opts[opt].groups_of) {
				Tcl_AppendResult(interp, "pset: incorrect "
				    "multiple of arguments", NULL);
				return (TCL_ERROR);
			}
		}

		if (dhcp_opts[opt].arg_size == 0)
			field_info.size = strlen(argv[1]);
		else
			field_info.size = dhcp_opts[opt].arg_size * (argc - 1);

		field_info.size += 2;	/* for code and size fields */

		/* now we need to figure out field_info.offset */

		if (dhcp_pkt.opts[opt] == NULL) { /* option is not there yet */

			uchar_t overload = dhcp_pkt.opts[CD_OPTION_OVERLOAD] ?
				*dhcp_pkt.opts[CD_OPTION_OVERLOAD]->value : 0;

			field_info.size++;	/* for the CD_END */

			if (get_opt_offset(&field_info, dpckt, overload) == 0) {
				Tcl_AppendResult(interp, "pset: no room left in"
				    " packet for option", NULL);
				return (TCL_ERROR);
			}

			if (field_info.offset + field_info.size > pckt->p_len)
				pckt->p_len = field_info.offset +
				    field_info.size;

			field_info.size--;	/* handled the CD_END */
			dpckt->uraw[field_info.offset+field_info.size] = CD_END;

		} else {			/* option is already there */

			/*
			 * if we can fit the new value for the option, it will
			 * go right where the old one is.  the only work we're
			 * gonna have to do is shift the other args if there's
			 * room to put the new value in.
			 */

			caddr_t cur_opt	  = (caddr_t)dhcp_pkt.opts[opt];
			field_info.offset = cur_opt - dpckt->raw;

			if (resize_opt(interp, dpckt, cur_opt, opt,
			    field_info.size, pckt) == TCL_ERROR)
				return (TCL_ERROR);
		}
		/* put code and len fields on, and call the to_raw() routine */

		field_info.size -= 2;	/* don't count code and len bytes */
		dpckt->raw[field_info.offset++] = (char)opt;
		dpckt->raw[field_info.offset++] = (char)field_info.size;

		if (field_info.to_raw(&field_info, dpckt, argc, argv) == 0) {
			Tcl_AppendResult(interp, "pset: input garbled", NULL);
			return (TCL_ERROR);
		}
	}
	return (TCL_OK);
}

/*
 * list()
 *
 * purpose: prints out all fields of the packet
 *   input: int		   : index of dhcp's layer in the stack for this packet
 *	    ClientData	   : <not used>
 *	    Tcl_Interp *   : the current tcl interpreter
 *	    struct packet *: the current packet, including the dhcp part
 *	    int		   : the argument count from psh `plist'
 *	    char *[]	   : the argument vector from psh `plist'
 *  output: int: TCL_OK on success, TCL_ERROR on failure
 */

int
list(int el, ClientData clientData, Tcl_Interp *interp, struct packet *pckt,
    int argc, char *argv[])
{
	size_t			 i;
	static char		 tmp[BUFFSIZE];
	struct dhcp_field_info	 field_info;
	struct dhcp_list	 dhcp_pkt;
	union  dhcp_un		*dpckt =
	    (union dhcp_un *)(pckt->p_buf + pckt->p_tbl[el].pt_offset);

	get_opts(dpckt, &dhcp_pkt, DHCP_LEN(pckt, el));

	/* go through all the fields in the packet and print them */
	for (i = 0; i < N_ITEMS(dhcp_fields); i++) {
		(void) __snprintf(tmp, sizeof (tmp), "\n%6s: ",
		    dhcp_fields[i].name);
		Tcl_AppendResult(interp, tmp, NULL);
		if (dhcp_fields[i].from_raw)
			dhcp_fields[i].from_raw(interp, &dhcp_fields[i], dpckt);
	}

	Tcl_AppendResult(interp, "\n\ndhcp options\n============\n", NULL);

	for (i = 1; i <= DHCP_LAST_OPT; i++) {

		/* list only the options that are set */
		if (dhcp_pkt.opts[i]) {

			(void) __snprintf(tmp, sizeof (tmp), "\n%26s: ",
			    dhcp_opts[i].name ? dhcp_opts[i].name : "<???>");
			Tcl_AppendResult(interp, tmp, NULL);

			make_field_info(&field_info, &dhcp_opts[i],
			    dhcp_pkt.opts[i], dpckt, pckt);

			/* if we don't know how to display it, display it raw */
			if (dhcp_opts[i].from_raw != NULL)
				field_info.from_raw(interp, &field_info, dpckt);
			else
				from_keep_raw(interp, &field_info, dpckt);
		}
	}
	return (TCL_OK);
}

/*
 * conversion functions, as previously discussed
 */

static void
from_keep_raw(Tcl_Interp *interp, const struct dhcp_field_info *field,
    const union dhcp_un *dpckt)
{
	char	tmp[6];
	uchar_t	len    = field->size;
	size_t	offset = field->offset;

	while (len--) {
		(void) __snprintf(tmp, sizeof (tmp), "0x%.2x ",
		    dpckt->uraw[offset++]);
		Tcl_AppendResult(interp, tmp, NULL);
	}
}

static bool_t
to_keep_raw(const struct dhcp_field_info *field, union dhcp_un *dpckt,
    int argc, char *argv[])
{
	uchar_t	len	= field->size;
	size_t	offset	= field->offset;

	for (++argv; *argv && len; len--, argv++)
		dpckt->raw[offset++] = strtoul(*argv, NULL, 0);

	return (BTRUE);
}

static void
from_int(Tcl_Interp *interp, const struct dhcp_field_info *field,
    const union dhcp_un *dpckt, char is_signed)
{
	static char 	tmp[BUFFSIZE];
	caddr_t		offset	= (caddr_t)dpckt->raw + field->offset;
	uint32_t	val32;
	uint16_t	val16;
	uint8_t		val8;

	switch (field->size) {
	case 1:
		FROM_DATA(&val8, offset);
		val32 = val8;
		break;
	case 2:
		FROM_DATA(&val16, offset);
		val32 = ntohs(val16);
		break;
	case 4:
		FROM_DATA(&val32, offset);
		val32 = ntohl(val32);
		break;
	default:
		return;
	}
	(void) __snprintf(tmp, sizeof (tmp), is_signed ? "0x%x (%d decimal)" :
	    "0x%x (%u decimal)", val32, val32);
	Tcl_AppendResult(interp, tmp, NULL);
}

static void
from_uint(Tcl_Interp *interp, const struct dhcp_field_info *field,
    const union dhcp_un *dpckt)
{
	from_int(interp, field, dpckt, 0);
}

static void
from_sint(Tcl_Interp *interp, const struct dhcp_field_info *field,
    const union dhcp_un *dpckt)
{
	from_int(interp, field, dpckt, 1);
}

static bool_t
to_uint(const struct dhcp_field_info *field, union dhcp_un *dpckt,
    int argc, char *argv[])
{
	caddr_t		offset	= dpckt->raw + field->offset;
	uint32_t	val32	= htonl(strtoul(argv[1], NULL, 0));
	uint16_t	val16	= htons(strtoul(argv[1], NULL, 0));
	uint8_t		val8	= strtoul(argv[1], NULL, 0);

	switch (field->size) {
	case 1:
		TO_DATA(offset, &val8);
		break;
	case 2:
		TO_DATA(offset, &val16);
		break;
	case 4:
		TO_DATA(offset, &val32);
		break;
	default:
		return (BFALSE);
	}
	return (BTRUE);
}

static void
from_chaddr(Tcl_Interp *interp, const struct dhcp_field_info *field,
    const union dhcp_un *dpckt)
{
	struct dhcp_field_info 	htype_info;

	(void) memcpy(&htype_info, field, sizeof (htype_info));
	htype_info.size = dpckt->dhcp.hlen;
	from_keep_raw(interp, &htype_info, dpckt);
}

static void
from_str(Tcl_Interp *interp, const struct dhcp_field_info *field,
    const union dhcp_un *dpckt)
{
	/*
	 * since we're not guaranteed that the string is \0-terminated,
	 * we change the character immediately following this field to
	 * \0 and change it back to its original value afterwards.
	 */

	caddr_t	endp	= (caddr_t)dpckt->raw + field->offset + field->size;
	char	c	= *endp;

	*endp = '\0';
	Tcl_AppendResult(interp, "\"", dpckt->raw + field->offset, "\"", NULL);
	*endp = c;
}

static bool_t
to_str(const struct dhcp_field_info *field, union dhcp_un *dpckt,
    int argc, char *argv[])
{
	/*
	 * copy the number of bytes this field has. use strncpy()'s
	 * obscure property that it will pad with NUL's
	 */

	(void) strncpy(dpckt->raw + field->offset, argv[1], field->size);
	return (BTRUE);
}

static void
from_htype(Tcl_Interp *interp, const struct dhcp_field_info *field,
    const union dhcp_un *dpckt)
{
	from_enum(interp, dpckt->dhcp.htype, htypes, N_ITEMS(htypes));
}

static bool_t
to_htype(const struct dhcp_field_info *field, union dhcp_un *dpckt,
    int argc, char *argv[])
{
	dpckt->dhcp.htype = to_enum(argv[1], htypes, N_ITEMS(htypes));
	return (BTRUE);
}

static void
from_op(Tcl_Interp *interp, const struct dhcp_field_info *field,
    const union dhcp_un *dpckt)
{
	from_enum(interp, dpckt->dhcp.op, ops, N_ITEMS(ops));
}

static bool_t
to_op(const struct dhcp_field_info *field, union dhcp_un *dpckt,
    int argc, char *argv[])
{
	dpckt->dhcp.op = to_enum(argv[1], ops, N_ITEMS(ops));
	return (BTRUE);
}

static void
from_bool(Tcl_Interp *interp, const struct dhcp_field_info *field,
    const union dhcp_un *dpckt)
{
	Tcl_AppendResult(interp, dpckt->raw[field->offset] ? "on" : "off",
	    NULL);
}

static bool_t
to_bool(const struct dhcp_field_info *field, union dhcp_un *dpckt,
    int argc, char *argv[])
{
	size_t	i;
	static const char *truth[] = {
	    "false",	"true",
	    "off",	"on",
	    "no",	"yes",
	    "0",	"1"
	};

	for (i = 0; i < N_ITEMS(truth); i++)
		if (strcasecmp(argv[1], truth[i]) == 0) {
			dpckt->raw[field->offset] = i % 2;
			return (BTRUE);
		}

	return (BFALSE);
}

static void
from_list(Tcl_Interp *interp, const struct dhcp_field_info *field,
    const union dhcp_un *dpckt, from_func *item_from_raw, size_t item_sz)
{
	size_t			i;
	struct dhcp_field_info	from_item_info;

	/*
	 * item_from_raw() will do most of the work, but we need to tune
	 * the `offset' and `size' parts of a dhcp_field_info for it
	 */

	(void) memcpy(&from_item_info, field, sizeof (from_item_info));
	from_item_info.size = item_sz;

	for (i = field->size / from_item_info.size; i != 0; i--) {
		item_from_raw(interp, &from_item_info, dpckt);
		Tcl_AppendResult(interp, (i != 1) ? ", " : "", NULL);
		from_item_info.offset += from_item_info.size;
	}
}

static bool_t
to_list(const struct dhcp_field_info *field, union dhcp_un *dpckt,
    int argc, char *argv[], to_func *item_to_raw, size_t item_sz)
{
	struct dhcp_field_info	to_item_info;

	(void) memcpy(&to_item_info, field, sizeof (to_item_info));
	to_item_info.size = item_sz;

	for (; argc > 1; ++argv, --argc) {
		if (item_to_raw(&to_item_info, dpckt, argc, argv) == 0)
			return (BFALSE);
		to_item_info.offset += to_item_info.size;
	}
	return (BTRUE);
}

static void
from_ip_list(Tcl_Interp *interp, const struct dhcp_field_info *field,
    const union dhcp_un *dpckt)
{
	from_list(interp, field, dpckt, from_ip, sizeof (struct in_addr));
}

static bool_t
to_ip_list(const struct dhcp_field_info *field, union dhcp_un *dpckt,
    int argc, char *argv[])
{
	return (to_list(field, dpckt, argc, argv, to_ip,
	    sizeof (struct in_addr)));
}

static void
from_u16_list(Tcl_Interp *interp, const struct dhcp_field_info *field,
    const union dhcp_un *dpckt)
{
	from_list(interp, field, dpckt, from_uint, 2);
}

static bool_t
to_u16_list(const struct dhcp_field_info *field, union dhcp_un *dpckt,
    int argc, char *argv[])
{
	return (to_list(field, dpckt, argc, argv, to_uint, 2));
}

static void
from_ip(Tcl_Interp *interp, const struct dhcp_field_info *field,
    const union dhcp_un *dpckt)
{
	struct hostent *hp;
	struct in_addr	addr;

	FROM_DATA(&addr, dpckt->raw + field->offset);

	/* we can always print it as a inet addr */
	Tcl_AppendResult(interp, inet_ntoa(addr), NULL);

	/* let's try to also print it as a hostname */
	hp = gethostbyaddr((caddr_t)&addr, sizeof (addr), AF_INET);
	if (hp != NULL)
		Tcl_AppendResult(interp, " (", hp->h_name, ")", NULL);
}

static bool_t
to_ip(const struct dhcp_field_info *field, union dhcp_un *dpckt,
    int argc, char *argv[])
{
	struct hostent *hp;
	struct in_addr	addr;

	/* first, let's try to decode it as dotted-decimal */
	addr.s_addr = inet_addr(argv[1]);
	if (addr.s_addr == (ulong_t)-1) {

		/* perhaps its a hostname */
		if ((hp = gethostbyname(argv[1])) == NULL)
			return (BFALSE);

		addr = *(struct in_addr *)*hp->h_addr_list;
	}

	TO_DATA(dpckt->raw + field->offset, &addr);
	return (BTRUE);
}	

static void
from_nb_tcpnt(Tcl_Interp *interp, const struct dhcp_field_info *field,
    const union dhcp_un *dpckt)
{
	from_enum(interp, dpckt->raw[field->offset], netbios_node_types,
	    N_ITEMS(netbios_node_types));
}

static bool_t
to_nb_tcpnt(const struct dhcp_field_info *field, union dhcp_un *dpckt,
    int argc, char *argv[])
{
	dpckt->raw[field->offset] = to_enum(argv[1], netbios_node_types,
	    N_ITEMS(netbios_node_types));
	return (BTRUE);
}


static void
from_dhcp_msg(Tcl_Interp *interp, const struct dhcp_field_info *field,
    const union dhcp_un *dpckt)
{
	from_enum(interp, dpckt->raw[field->offset], dhcp_msg_types,
	    N_ITEMS(dhcp_msg_types));
}

static bool_t
to_dhcp_msg(const struct dhcp_field_info *field, union dhcp_un *dpckt,
    int argc, char *argv[])
{
	dpckt->raw[field->offset] = to_enum(argv[1], dhcp_msg_types,
	    N_ITEMS(dhcp_msg_types));
	return (BTRUE);
}

static void
from_opt_over(Tcl_Interp *interp, const struct dhcp_field_info *field,
    const union dhcp_un *dpckt)
{
	from_enum(interp, dpckt->raw[field->offset], overloads,
	    N_ITEMS(overloads));
}

static bool_t
to_opt_over(const struct dhcp_field_info *field, union dhcp_un *dpckt,
    int argc, char *argv[])
{
	dpckt->raw[field->offset] = to_enum(argv[1], overloads,
	    N_ITEMS(overloads));
	return (BTRUE);
}

static void
from_flags(Tcl_Interp *interp, const struct dhcp_field_info *field,
    const union dhcp_un *dpckt)
{
	const char *state = (ntohs(dpckt->dhcp.flags) &BCAST_ON) ? "on" : "off";

	from_uint(interp, field, dpckt);
	Tcl_AppendResult(interp, " (broadcast ", state, ")", NULL);
}
