/*
arp.c

Copyright 1995 Philip Homburg
*/

#include "inet.h"
#include "type.h"

#include "arp.h"
#include "assert.h"
#include "buf.h"
#include "clock.h"
#include "eth.h"
#include "io.h"
#include "sr.h"

THIS_FILE

#define ARP_CACHE_NR	64

#define MAX_ARP_RETRIES		5
#define ARP_TIMEOUT		(HZ/2+1)	/* .5 seconds */
#ifndef ARP_EXP_TIME
#define ARP_EXP_TIME		(20L*60L*HZ)	/* 20 minutes */
#endif
#define ARP_NOTRCH_EXP_TIME	(5*HZ)		/* 5 seconds */
#define ARP_INUSE_OFFSET	(60*HZ)	/* an entry in the cache can be deleted
					   if its not used for 1 minute */

typedef struct arp46
{
	ether_addr_t a46_dstaddr;
	ether_addr_t a46_srcaddr;
	ether_type_t a46_ethtype;
	union
	{
		struct
		{
			u16_t a_hdr, a_pro;
			u8_t a_hln, a_pln;
			u16_t a_op;
			ether_addr_t a_sha;
			u8_t a_spa[4];
			ether_addr_t a_tha;
			u8_t a_tpa[4];
		} a46_data;
		char    a46_dummy[ETH_MIN_PACK_SIZE-ETH_HDR_SIZE];
	} a46_data;
} arp46_t;

#define a46_hdr a46_data.a46_data.a_hdr
#define a46_pro a46_data.a46_data.a_pro
#define a46_hln a46_data.a46_data.a_hln
#define a46_pln a46_data.a46_data.a_pln
#define a46_op a46_data.a46_data.a_op
#define a46_sha a46_data.a46_data.a_sha
#define a46_spa a46_data.a46_data.a_spa
#define a46_tha a46_data.a46_data.a_tha
#define a46_tpa a46_data.a46_data.a_tpa

typedef struct arp_port
{
	int ap_flags;
	int ap_state;
	int ap_eth_port;
	int ap_eth_fd;
	ether_addr_t ap_ethaddr;
	ipaddr_t ap_ipaddr;
	timer_t ap_timer;

	ether_addr_t ap_write_ethaddr;
	ipaddr_t ap_write_ipaddr;
	int ap_write_code;

	ipaddr_t ap_req_ipaddr;
	int ap_req_count;

	arp_func_t ap_arp_func;
} arp_port_t;

#define APF_EMPTY	0
#define APF_ARP_RD_IP	0x4
#define APF_ARP_RD_SP	0x8
#define APF_ARP_WR_IP	0x10
#define APF_ARP_WR_SP	0x20
#define APF_INADDR_SET	0x100
#define APF_MORE2WRITE	0x200
#define APF_CLIENTREQ	0x400
#define APF_CLIENTWRITE	0x1000
#define APF_SUSPEND	0x2000

#define APS_INITIAL	0x00
#define	APS_GETADDR	0x01
#define	APS_ARPSTART	0x10
#define	APS_ARPPROTO	0x20
#define	APS_ARPMAIN	0x40
#define	APS_ERROR	0x80

typedef struct arp_cache
{
	int ac_flags;
	int ac_state;
	ether_addr_t ac_ethaddr;
	ipaddr_t ac_ipaddr;
	arp_port_t *ac_port;
	time_t ac_expire;
	time_t ac_lastuse;
} arp_cache_t;

#define ACF_EMPTY	0
#define ACF_GOTREQ	1

#define ACS_UNUSED	0
#define ACS_INCOMPLETE	1
#define ACS_VALID	2
#define ACS_UNREACHABLE	3

FORWARD acc_t *arp_getdata ARGS(( int fd, size_t offset,
	size_t count, int for_ioctl ));
FORWARD int arp_putdata ARGS(( int fd, size_t offset,
	acc_t *data, int for_ioctl ));
FORWARD void arp_main ARGS(( arp_port_t *arp_port ));
FORWARD void arp_timeout ARGS(( int fd, timer_t *timer ));
FORWARD void setup_write ARGS(( arp_port_t *arp_port ));
FORWARD void setup_read ARGS(( arp_port_t *arp_port ));
FORWARD void process_arp_req ARGS(( arp_port_t *arp_port, acc_t *data ));
FORWARD void client_reply ARGS(( arp_port_t *arp_port,
	ipaddr_t ipaddr, ether_addr_t *ethaddr ));
FORWARD arp_cache_t *find_cache_ent ARGS(( arp_port_t *arp_port,
	ipaddr_t ipaddr ));
FORWARD arp_cache_t *alloc_cache_ent ARGS(( void ));

PRIVATE arp_port_t arp_port_table[ARP_PORT_NR];
/* PRIVATE arp_port_t *arp_port; */
PRIVATE	arp_cache_t arp_cache[ARP_CACHE_NR];

PUBLIC void arp_init()
{
	arp_port_t *arp_port;
	int i;

	assert (BUF_S >= sizeof(struct nwio_ethstat));
	assert (BUF_S >= sizeof(struct nwio_ethopt));
	assert (BUF_S >= sizeof(arp46_t));

	for (i=0, arp_port= arp_port_table; i<ARP_PORT_NR; i++, arp_port++)
	{
		arp_port->ap_state= APS_ERROR;	/* Mark all ports as
						 * unavailable */
	}

}

PRIVATE void arp_main(arp_port)
arp_port_t *arp_port;
{
	int result;

	switch (arp_port->ap_state)
	{
	case APS_INITIAL:
		arp_port->ap_eth_fd= eth_open(arp_port->ap_eth_port,
			arp_port-arp_port_table, arp_getdata, arp_putdata, 0);

		if (arp_port->ap_eth_fd<0)
		{
			DBLOCK(1, printf("arp.c: unable to open ethernet\n"));
			return;
		}

		arp_port->ap_state= APS_GETADDR;

		result= eth_ioctl (arp_port->ap_eth_fd, NWIOGETHSTAT);

		if ( result == NW_SUSPEND)
		{
			arp_port->ap_flags |= APF_SUSPEND;
			return;
		}
		assert(result == NW_OK);

		/* fall through */
	case APS_GETADDR:
		/* Wait for IP address */
		if (!(arp_port->ap_flags & APF_INADDR_SET))
			return;

		/* fall through */
	case APS_ARPSTART:
		arp_port->ap_state= APS_ARPPROTO;

		{
			arp_cache_t *cache;
			int i;

			cache= arp_cache;
			for (i=0; i<ARP_CACHE_NR; i++, cache++)
			{
				cache->ac_state= ACS_UNUSED;
				cache->ac_flags= ACF_EMPTY;
				cache->ac_expire= 0;
				cache->ac_lastuse= 0;
			}
		}
		result= eth_ioctl (arp_port->ap_eth_fd, NWIOSETHOPT);

		if (result==NW_SUSPEND)
		{
			arp_port->ap_flags |= APF_SUSPEND;
			return;
		}
		assert(result == NW_OK);

		/* fall through */
	case APS_ARPPROTO:
		arp_port->ap_state= APS_ARPMAIN;
		if (arp_port->ap_flags & APF_MORE2WRITE)
			setup_write(arp_port);
		setup_read(arp_port);
		return;

	default:
		ip_panic((
		 "arp_main(&arp_port_table[%d]) called but ap_state=0x%x\n",
			arp_port-arp_port_table, arp_port->ap_state ));
	}
}

PRIVATE acc_t *arp_getdata (fd, offset, count, for_ioctl)
int fd;
size_t offset;
size_t count;
int for_ioctl;
{
	arp_port_t *arp_port;
	arp46_t *arp;
	acc_t *data;
	int result;

	arp_port= &arp_port_table[fd];

	switch (arp_port->ap_state)
	{
	case APS_ARPPROTO:
		if (!count)
		{
			result= (int)offset;
			if (result<0)
			{
				arp_port->ap_state= APS_ERROR;
				break;
			}
			if (arp_port->ap_flags & APF_SUSPEND)
			{
				arp_port->ap_flags &= ~APF_SUSPEND;
				arp_main(arp_port);
			}
			return NW_OK;
		}
		assert ((!offset) && (count == sizeof(struct nwio_ethopt)));
		{
			struct nwio_ethopt *ethopt;
			acc_t *acc;

			acc= bf_memreq(sizeof(*ethopt));
			ethopt= (struct nwio_ethopt *)ptr2acc_data(acc);
			ethopt->nweo_flags= NWEO_COPY|NWEO_EN_BROAD|
				NWEO_TYPESPEC;
			ethopt->nweo_type= HTONS(ETH_ARP_PROTO);
			return acc;
		}
	case APS_ARPMAIN:
		assert (arp_port->ap_flags & APF_ARP_WR_IP);
		if (!count)
		{
			result= (int)offset;
			if (result<0)
			{
				DIFBLOCK(1, (result != NW_SUSPEND),
					printf(
				"arp.c: write error on port %d: error %d\n",
					fd, result));

				arp_port->ap_state= APS_ERROR;
				break;
			}
			arp_port->ap_flags &= ~APF_ARP_WR_IP;
			if (arp_port->ap_flags & APF_ARP_WR_SP)
				setup_write(arp_port);
			return NW_OK;
		}
		assert (offset+count <= sizeof(arp46_t));
		data= bf_memreq(sizeof(arp46_t));
		arp= (arp46_t *)ptr2acc_data(data);
		data->acc_offset += offset;
		data->acc_length= count;
		if (arp_port->ap_write_code == ARP_REPLY)
			arp->a46_dstaddr= arp_port->ap_write_ethaddr;
		else
		{
			arp->a46_dstaddr.ea_addr[0]= 0xff;
			arp->a46_dstaddr.ea_addr[1]= 0xff;
			arp->a46_dstaddr.ea_addr[2]= 0xff;
			arp->a46_dstaddr.ea_addr[3]= 0xff;
			arp->a46_dstaddr.ea_addr[4]= 0xff;
			arp->a46_dstaddr.ea_addr[5]= 0xff;
		}
		arp->a46_hdr= HTONS(ARP_ETHERNET);
		arp->a46_pro= HTONS(ETH_IP_PROTO);
		arp->a46_hln= 6;
		arp->a46_pln= 4;
		arp->a46_op= htons(arp_port->ap_write_code);
		arp->a46_sha= arp_port->ap_ethaddr;
		memcpy (arp->a46_spa, &arp_port->ap_ipaddr, sizeof(ipaddr_t));
		arp->a46_tha= arp_port->ap_write_ethaddr;
		memcpy (arp->a46_tpa, &arp_port->ap_write_ipaddr,
			sizeof(ipaddr_t));
		return data;
	default:
#if !CRAMPED
		printf("arp_getdata(%d, 0x%d, 0x%d) called but ap_state=0x%x\n",
			fd, offset, count, arp_port->ap_state);
#endif
		break;
	}
	return 0;
}

PRIVATE int arp_putdata (fd, offset, data, for_ioctl)
int fd;
size_t offset;
acc_t *data;
int for_ioctl;
{
	arp_port_t *arp_port;
	int result;
	struct nwio_ethstat *ethstat;

	arp_port= &arp_port_table[fd];

	if (arp_port->ap_flags & APF_ARP_RD_IP)
	{
		if (!data)
		{
			result= (int)offset;
			if (result<0)
			{
				DIFBLOCK(1, (result != NW_SUSPEND), printf(
				"arp.c: read error on port %d: error %d\n",
					fd, result));

				return NW_OK;
			}
			if (arp_port->ap_flags & APF_ARP_RD_SP)
			{
				arp_port->ap_flags &= ~(APF_ARP_RD_IP|
					APF_ARP_RD_SP);
				setup_read(arp_port);
			}
			else
				arp_port->ap_flags &= ~(APF_ARP_RD_IP|
					APF_ARP_RD_SP);
			return NW_OK;
		}
		assert (!offset);
		/* Warning: the above assertion is illegal; puts and gets of
		   data can be brokenup in any piece the server likes. However
		   we assume that the server is eth.c and it transfers only
		   whole packets. */
		data= bf_packIffLess(data, sizeof(arp46_t));
		if (data->acc_length >= sizeof(arp46_t))
			process_arp_req(arp_port,data);
		bf_afree(data);
		return NW_OK;
	}
	switch (arp_port->ap_state)
	{
	case APS_GETADDR:
		if (!data)
		{
			result= (int)offset;
			if (result<0)
			{
				arp_port->ap_state= APS_ERROR;
				break;
			}
			if (arp_port->ap_flags & APF_SUSPEND)
			{
				arp_port->ap_flags &= ~APF_SUSPEND;
				arp_main(arp_port);
			}
			return NW_OK;
		}
		compare (bf_bufsize(data), ==, sizeof(*ethstat));
		data= bf_packIffLess(data, sizeof(*ethstat));
		compare (data->acc_length, ==, sizeof(*ethstat));
		ethstat= (struct nwio_ethstat *)ptr2acc_data(data);
		arp_port->ap_ethaddr= ethstat->nwes_addr;
		bf_afree(data);
		return NW_OK;
	default:
#if !CRAMPED
		printf("arp_putdata(%d, 0x%d, 0x%lx) called but ap_state=0x%x\n",
			fd, offset, (unsigned long)data, arp_port->ap_state);
#endif
		break;
	}
	return EGENERIC;
}

PRIVATE void setup_read(arp_port)
arp_port_t *arp_port;
{
	int result;

	while (!(arp_port->ap_flags & APF_ARP_RD_IP))
	{
		arp_port->ap_flags |= APF_ARP_RD_IP;
		result= eth_read (arp_port->ap_eth_fd, ETH_MAX_PACK_SIZE);
		if (result == NW_SUSPEND)
		{
			arp_port->ap_flags |= APF_ARP_RD_SP;
			return;
		}
		DIFBLOCK(1, (result != NW_OK),
			printf("arp.c: eth_read(..,%d)=%d\n",
			ETH_MAX_PACK_SIZE, result));
	}
}

PRIVATE void setup_write(arp_port)
arp_port_t *arp_port;
{
	int i, result;

	while (arp_port->ap_flags & APF_MORE2WRITE)
	{
		if (arp_port->ap_flags & APF_CLIENTWRITE)
		{
			arp_port->ap_flags &= ~APF_CLIENTWRITE;
			arp_port->ap_write_ipaddr= arp_port->ap_req_ipaddr;
			arp_port->ap_write_code= ARP_REQUEST;
			clck_timer(&arp_port->ap_timer,
				get_time() + ARP_TIMEOUT,
				arp_timeout, arp_port-arp_port_table);
		}
		else
		{
			arp_cache_t *cache;

			cache= arp_cache;
			for (i=0; i<ARP_CACHE_NR; i++, cache++)
			{
				if ((cache->ac_flags & ACF_GOTREQ) &&
					cache->ac_port == arp_port)
				{
					cache->ac_flags &= ~ACF_GOTREQ;
					arp_port->ap_write_ethaddr= cache->
						ac_ethaddr;
					arp_port->ap_write_ipaddr= cache->
						ac_ipaddr;
					arp_port->ap_write_code= ARP_REPLY;
					break;
				}
			}
			if (i>=ARP_CACHE_NR)
			{
				arp_port->ap_flags &= ~APF_MORE2WRITE;
				break;
			}
		}
		arp_port->ap_flags= (arp_port->ap_flags & ~APF_ARP_WR_SP) |
			APF_ARP_WR_IP;
		result= eth_write(arp_port->ap_eth_fd, sizeof(arp46_t));
		if (result == NW_SUSPEND)
			arp_port->ap_flags |= APF_ARP_WR_SP;
		if (result<0)
		{
			DIFBLOCK(1, (result != NW_SUSPEND),
				printf("arp.c: eth_write(..,%d)=%d\n",
				sizeof(arp46_t), result));
			return;
		}
	}
}

PRIVATE void process_arp_req (arp_port, data)
arp_port_t *arp_port;
acc_t *data;
{
	arp46_t *arp;
	arp_cache_t *ce;
	int level;
	time_t curr_time;
	ipaddr_t spa, tpa;

	curr_time= get_time();

	arp= (arp46_t *)ptr2acc_data(data);
	memcpy(&spa, arp->a46_spa, sizeof(ipaddr_t));
	memcpy(&tpa, arp->a46_tpa, sizeof(ipaddr_t));

	if (arp->a46_hdr != HTONS(ARP_ETHERNET) ||
		arp->a46_hln != 6 ||
		arp->a46_pro != HTONS(ETH_IP_PROTO) ||
		arp->a46_pln != 4)
		return;
	ce= find_cache_ent(arp_port, spa);
	if (ce && ce->ac_expire < curr_time)
	{
		DBLOCK(0x10, printf("arp: expiring entry for ");
			writeIpAddr(ce->ac_ipaddr); printf("\n"));
		ce->ac_state= ACS_UNUSED;
		ce= NULL;
	}
	if (ce == NULL)
	{
		if (tpa != arp_port->ap_ipaddr)
			return;

		DBLOCK(0x10, printf("arp: allocating entry for ");
			writeIpAddr(spa); printf("\n"));

		ce= alloc_cache_ent();
		ce->ac_flags= ACF_EMPTY;
		ce->ac_state= ACS_VALID;
		ce->ac_ethaddr= arp->a46_sha;
		ce->ac_ipaddr= spa;
		ce->ac_port= arp_port;
		ce->ac_expire= curr_time+ARP_EXP_TIME;
		ce->ac_lastuse= curr_time-ARP_INUSE_OFFSET; /* never used */
	}

	if (ce->ac_state == ACS_INCOMPLETE || ce->ac_state == ACS_UNREACHABLE)
	{
		ce->ac_ethaddr= arp->a46_sha;
		if (ce->ac_state == ACS_INCOMPLETE)
		{
			ce->ac_state= ACS_VALID;
			client_reply(arp_port, spa, &arp->a46_sha);
		}
		else
			ce->ac_state= ACS_VALID;
	}

	/* Update fields in the arp cache. */
#if !CRAMPED
	if (memcmp(&ce->ac_ethaddr, &arp->a46_sha,
		sizeof(ce->ac_ethaddr)) != 0)
	{
		printf("arp: ethernet address for IP address ");
		writeIpAddr(spa);
		printf(" changed from ");
		writeEtherAddr(&ce->ac_ethaddr);
		printf(" to ");
		writeEtherAddr(&arp->a46_sha);
		printf("\n");
		ce->ac_ethaddr= arp->a46_sha;
	}
#else
	ce->ac_ethaddr= arp->a46_sha;
#endif
	ce->ac_expire= curr_time+ARP_EXP_TIME;

	if (arp->a46_op == HTONS(ARP_REQUEST) && (tpa == arp_port->ap_ipaddr))
	{
		ce->ac_flags |= ACF_GOTREQ;
		arp_port->ap_flags |= APF_MORE2WRITE;
		if (!(arp_port->ap_flags & APF_ARP_WR_IP))
			setup_write(arp_port);
	}
}

PRIVATE void client_reply (arp_port, ipaddr, ethaddr)
arp_port_t *arp_port;
ipaddr_t ipaddr;
ether_addr_t *ethaddr;
{
	if ((arp_port->ap_flags & APF_CLIENTREQ) &&
		ipaddr == arp_port->ap_req_ipaddr)
	{
		arp_port->ap_flags &= ~(APF_CLIENTREQ|APF_CLIENTWRITE);
		clck_untimer(&arp_port->ap_timer);
	}
	(*arp_port->ap_arp_func)(arp_port-arp_port_table, ipaddr, ethaddr);
}

PRIVATE arp_cache_t *find_cache_ent (arp_port, ipaddr)
arp_port_t *arp_port;
ipaddr_t ipaddr;
{
	arp_cache_t *cache;
	int i;

	for (i=0, cache= arp_cache; i<ARP_CACHE_NR; i++, cache++)
	{
		if (cache->ac_state != ACS_UNUSED &&
			cache->ac_port == arp_port &&
			cache->ac_ipaddr == ipaddr)
		{
			return cache;
		}
	}
	return NULL;
}

PRIVATE arp_cache_t *alloc_cache_ent()
{
	arp_cache_t *cache, *old;
	int i;

	old= NULL;
	for (i=0, cache= arp_cache; i<ARP_CACHE_NR; i++, cache++)
	{
		if (cache->ac_state == ACS_UNUSED)
			return cache;
		if (cache->ac_state == ACS_INCOMPLETE)
			continue;
		if (!old || cache->ac_lastuse < old->ac_lastuse)
			old= cache;
	}
	assert(old);
	return old;
}

PUBLIC void arp_set_ipaddr (ip_port, ipaddr)
int ip_port;
ipaddr_t ipaddr;
{
	arp_port_t *arp_port;
	int i;

	if (ip_port < 0 || ip_port >= ARP_PORT_NR)
		return;
	arp_port= &arp_port_table[ip_port];

	arp_port->ap_ipaddr= ipaddr;
	arp_port->ap_flags |= APF_INADDR_SET;
	arp_port->ap_flags &= ~APF_SUSPEND;
	if (arp_port->ap_state == APS_GETADDR)
		arp_main(arp_port);
}

PUBLIC int arp_set_cb(ip_port, eth_port, arp_func)
int ip_port;
int eth_port;
arp_func_t arp_func;
{
	arp_port_t *arp_port;
	int i;

	assert(ip_port >= 0);
	if (ip_port >= ARP_PORT_NR)
		return ENXIO;

	arp_port= &arp_port_table[ip_port];
	arp_port->ap_eth_port= eth_port;
	arp_port->ap_state= APS_INITIAL;
	arp_port->ap_flags= APF_EMPTY;
	arp_port->ap_arp_func= arp_func;

	arp_main(arp_port);

	return NW_OK;
}

PUBLIC int arp_ip_eth (ip_port, ipaddr, ethaddr)
int ip_port;
ipaddr_t ipaddr;
ether_addr_t *ethaddr;
{
	arp_port_t *arp_port;
	int i;
	arp_cache_t *ce;
	time_t curr_time;

	assert(ip_port >= 0 && ip_port < ARP_PORT_NR);
	arp_port= &arp_port_table[ip_port];
	assert(arp_port->ap_state == APS_ARPMAIN ||
		(printf("ap_state= %d\n", arp_port->ap_state), 0));

	curr_time= get_time();

	ce= find_cache_ent (arp_port, ipaddr);
	if (ce && ce->ac_expire < curr_time)
	{
		ce->ac_state= ACS_UNUSED;
		ce= NULL;
	}
	if (ce)
	{
		/* Found an entry. This entry should be valid, unreachable
		 * or incomplete.
		 */
		ce->ac_lastuse= curr_time;
		if (ce->ac_state == ACS_VALID)
		{
			*ethaddr= ce->ac_ethaddr;
			return NW_OK;
		}
		if (ce->ac_state == ACS_UNREACHABLE)
			return EDSTNOTRCH;
		assert(ce->ac_state == ACS_INCOMPLETE);
		return NW_SUSPEND;
	}

	if (arp_port->ap_flags & APF_CLIENTREQ)
	{
		/* We should implement something to be able to do
		 * multiple arp lookups at the same time. At the moment
		 * we just return SUSPEND.
		 */
		return NW_SUSPEND;
	}
	ce= alloc_cache_ent();
	ce->ac_flags= 0;
	ce->ac_state= ACS_INCOMPLETE;
	ce->ac_ipaddr= ipaddr;
	ce->ac_port= arp_port;
	ce->ac_expire= curr_time+ARP_EXP_TIME;
	ce->ac_lastuse= curr_time;
	arp_port->ap_flags |= APF_CLIENTREQ|APF_MORE2WRITE | APF_CLIENTWRITE;
	arp_port->ap_req_ipaddr= ipaddr;
	arp_port->ap_req_count= 0;
	if (!(arp_port->ap_flags & APF_ARP_WR_IP))
		setup_write(arp_port);
	return NW_SUSPEND;
}

PRIVATE void arp_timeout (fd, timer)
int fd;
timer_t *timer;
{
	arp_port_t *arp_port;
	arp_cache_t *ce;
	int level;
	time_t curr_time;

	arp_port= &arp_port_table[fd];

	assert (timer == &arp_port->ap_timer);

	if (++arp_port->ap_req_count < MAX_ARP_RETRIES)
	{
		arp_port->ap_flags |= APF_CLIENTWRITE|APF_MORE2WRITE;
		if (!(arp_port->ap_flags & APF_ARP_WR_IP))
			setup_write(arp_port);
	}
	else
	{
		ce= find_cache_ent(arp_port, arp_port->ap_req_ipaddr);
		assert(ce && ce->ac_state == ACS_INCOMPLETE ||
			(printf("ce= %p, ce->ac_state= %d\n",
				ce, ce ? ce->ac_state : -1),0));
		curr_time= get_time();
		ce->ac_state= ACS_UNREACHABLE;
		ce->ac_expire= curr_time+ ARP_NOTRCH_EXP_TIME;
		ce->ac_lastuse= curr_time;

		client_reply(arp_port, ce->ac_ipaddr, NULL);
	}
}

/*
 * $PchId: arp.c,v 1.6 1995/11/21 06:45:27 philip Exp $
 */
