/* vim: set filetype=c:
 *
 * bootp.c86  -  MGL runtime BOOTP/DHCP record handling routines
 *
 * Copyright (C) 2003-2007 Gero Kuhlmann   <gero@gkminix.han.de>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * $Id: bootp.c86,v 1.16 2007/01/06 18:31:38 gkminix Exp $
 */

#include "rtcommon.h86"



/*
 *****************************************************************************
 *
 * Storage areas for tags
 */
#define BOOTP_AREA_ANY		-1	/* any tag area */
#define BOOTP_AREA_VEND		 0	/* vendor tag area */
#define BOOTP_AREA_SNAME	 1	/* server name area */
#define BOOTP_AREA_FILE		 2	/* file name area */

#define BOOTP_AREA_NUM		 3	/* number of tag storage areas */



/*
 *****************************************************************************
 *
 * Global variables
 */
char bootfile[BOOTP_FILE_SIZE + 1];
char servername[BOOTP_SNAME_SIZE + 1];
char hostname[MAX_HOST_NAME + 1];
char domainname[MAX_DOMAIN_NAME + 1];
t_ipaddr tftpaddr;
t_ipaddr gatewayaddr;
t_ipaddr dnsaddr;



/*
 *****************************************************************************
 *
 * Local variables
 */
static __nptr nextexit = NNULL;		/* Address to call next upon exit */
static __u8 overload = 0;		/* Overload flag */
static __u8 *pxestart = NULL;		/* Start of PXE tag */
static __u8 *pxeend = NULL;		/* End of PXE tag */
static __u8 *nbstart = NULL;		/* Start of netboot tag */
static __u8 *nbend = NULL;		/* End of netboot tag */
static __u8 *endtags[BOOTP_AREA_NUM];	/* Pointers to end tags */
static __u8 bootpmem[BOOTP_MAX_SIZE];	/* BOOTP record */
static struct bootp *bootp = (struct bootp *)bootpmem;



/*
 *****************************************************************************
 *
 * Check if an area is available
 */
static bool_t check_area(short area)
{
  switch (area) {
	case BOOTP_AREA_FILE:
		return(overload == VEND_OVR_FILE || overload == VEND_OVR_BOTH);
	case BOOTP_AREA_SNAME:
		return(overload == VEND_OVR_SNAME || overload == VEND_OVR_BOTH);
	case BOOTP_AREA_VEND:
		return(TRUE);
	default:
		break;
  }
  return(FALSE);
}



/*
 *****************************************************************************
 *
 * Determine the storage area of a tag
 */
static short get_area(const __u8 *tag)
{
  if (tag >= bootp->bp_file && tag < bootp->bp_vend)
	return(BOOTP_AREA_FILE);
  else if (tag >= bootp->bp_sname && tag < bootp->bp_file)
	return(BOOTP_AREA_SNAME);
  else if (tag >= &(bootp->bp_vend[BOOTP_VM_SIZE]))
	return(BOOTP_AREA_VEND);
  return(-1);
}



/*
 *****************************************************************************
 *
 * Determine start of tag area
 */
static __u8 *get_startptr(short area)
{
  switch (area) {
	case BOOTP_AREA_FILE:
		return(bootp->bp_file);
	case BOOTP_AREA_SNAME:
		return(bootp->bp_sname);
	case BOOTP_AREA_VEND:
		return(&(bootp->bp_vend[BOOTP_VM_SIZE]));
	default:
		break;
  }
  return(NULL);
}



/*
 *****************************************************************************
 *
 * Determine end of tag area
 */
static __u8 *get_endptr(short area)
{
  switch (area) {
	case BOOTP_AREA_FILE:
		return(bootp->bp_vend);
	case BOOTP_AREA_SNAME:
		return(bootp->bp_file);
	case BOOTP_AREA_VEND:
		return(bootpmem + BOOTP_MAX_SIZE);
	default:
		break;
  }
  return(NULL);
}



/*
 *****************************************************************************
 *
 * Search for a vendor tag. This routine returns a pointer to the
 * tag ID byte.
 */
static __u8 *search_tag(short id, __u8 *start, const __u8 *end)
{
  size_t len;

  if (id == VEND_RFC_NOP)
	return(NULL);

  while (*start != (__u8)VEND_RFC_END) {
	if (*start == (__u8)VEND_RFC_NOP) {
		start++;
		continue;
	}
	if (*start == (__u8)(id & 0x00FF))
		return(start);
	len = (size_t)(*(start + 1)) + 2;
	if ((start + len) >= end)
		break;
	start += len;
  }
  return(id == VEND_RFC_END ? start : NULL);
}



/*
 *****************************************************************************
 *
 * Find all end tags
 */
static void find_endtags(void)
{
  __u8 *start, *end, *bp;
  short area;

  for (area = 0; area < BOOTP_AREA_NUM; area++) {
	endtags[area] = NULL;
	start = get_startptr(area);
	end = get_endptr(area);
	if (check_area(area)) {
		bp = search_tag(VEND_RFC_END, start, end);
		if (bp != NULL) {
			*bp = (__u8)VEND_RFC_END;
			endtags[area] = bp;
		}
	}
  }
}



/*
 *****************************************************************************
 *
 * Find a BOOTP tag. This also finds PXE tags, and returns the start of a tag.
 */
static __u8 *find_tag(short usearea, short id, size_t *blen)
{
  __u8 *start, *end, *bp;
  short area;

  bp = NULL;
  if (id > VEND_RFC_END && usearea == BOOTP_AREA_ANY) {
	if (id > VEND_PXE_BASE && id <= VEND_PXE_END) {
		start = pxestart;
		end = pxeend;
	} else if (id > VEND_NB_BASE && id <= VEND_NB_END) {
		start = nbstart;
		end = nbend;
	} else {
		start = NULL;
		end = NULL;
	}
	id &= 0x00ff;
	if (start != NULL && end != NULL)
		bp = search_tag(id, start, end);
  } else for (area = 0; area < BOOTP_AREA_NUM; area++) {
	if (usearea != BOOTP_AREA_ANY && usearea != area)
		continue;
	start = get_startptr(area);
	end = get_endptr(area);
	if (check_area(area)) {
		bp = search_tag(id, start, end);
		if (bp != NULL)
			break;
	}
  }

  *blen = (size_t)0;
  if (bp != NULL && id != VEND_RFC_NOP && id != VEND_RFC_END) {
	*blen = (size_t)(bp[1]);
	bp += 2;
  }
  return(bp);
}



/*
 *****************************************************************************
 *
 * Delete a vendor tag
 */
static void delete_tag(__u8 *tag)
{
  __u8 *bp1, *bp2, *end;
  size_t len, i;
  short area;

  bp1 = tag - 2;
  area = get_area(bp1);
  if (!check_area(area))
	return;

  end = get_endptr(area);
  len = (size_t)(*(bp1 + 1)) + 2;
  bp2 = bp1 + len;
  while (*bp2 != (__u8)VEND_RFC_END) {
	if (*bp2 == (__u8)VEND_RFC_NOP) {
		*bp1++ = *bp2++;
		continue;
	}
	len = (size_t)(*(bp2 + 1)) + 2;
	if ((bp2 + len) >= end)
		break;
	for (i = 0; i < len; i++)
		*bp1++ = *bp2++;
  }
  *bp1 = (__u8)VEND_RFC_END;
  endtags[area] = bp1;
}



/*
 *****************************************************************************
 *
 * Add a new vendor tag
 */
static __u8 *add_tag(short usearea, short id, size_t size)
{
  __u8 *start, *end, *cp;
  size_t remaining, slen;
  short area;

  /* Check for correct parameters, there is no support for PXE tags here */
  if (id <= VEND_RFC_NOP || id >= VEND_RFC_END || size > 255)
	return(NULL);

  /* Find a storage area which has enough free space */
  start = NULL;
  end = NULL;
  for (area = 0; area < BOOTP_AREA_NUM; area++) {
	start = get_startptr(area);
	end = get_endptr(area);
	if ((usearea == BOOTP_AREA_ANY || usearea == area) &&
	    (check_area(area) && endtags[area] != NULL)) {
		remaining = (size_t)(end - endtags[area]) - 1;
		if (remaining >= (size + 2))
			break;
	}
  }

  /* Check if we need to make some free space */
  if (usearea != BOOTP_AREA_ANY && usearea != area)
	return(NULL);

  /* If we could not find a free area, try to make space */
  if (area >= BOOTP_AREA_NUM) {
	/*
	 * Scan through string areas and check if we can convert the string
	 * into a tag, and thus make it's memoyr space available for other
	 * tags as well.
	 */
	cp = NULL;
	start = NULL;
	area = BOOTP_AREA_SNAME;
	while (area != BOOTP_AREA_VEND) {
		start = get_startptr(area);
		end = get_endptr(area);
		if (!check_area(area)) {
			cp = start;
			while (cp < end && *cp)
				cp++;
			remaining = (cp >= end) ? 0 : (size_t)(end - cp) - 1;
			/*
			 * We need enough space for:
			 *   the tag value itself                --> (size)
			 *   the tag header (ID and size byte)   --> 2
			 *   the header for the converted string --> 2
			 *   the end tag                         --> 1
			 */
			if (remaining >= (size + 2 + 2 + 1))
				break;
		}
		if (area == BOOTP_AREA_SNAME)
			area = BOOTP_AREA_FILE;
		else
			area = BOOTP_AREA_VEND;
	}
	/* If we can't convert a string area, we are out of memory */
	if (area == BOOTP_AREA_VEND || cp == NULL || start == NULL)
		return(NULL);
	/*
	 * Now convert the string in the area into a tag, and mark this area
	 * as overloaded. If there is no string in the specified area, we
	 * don't need to create a new tag.
	 */
	if (cp == start)
		*cp = (__u8)VEND_RFC_END;
	else {
		slen = 0;
		*(cp + 3) = (__u8)VEND_RFC_END;
		endtags[area] = (cp + 3);
		while (cp >= start) {
			*(cp + 2) = *cp;
			slen++;
			cp--;
		}
		cp[0] = (area == BOOTP_AREA_SNAME) ? (__u8)VEND_RFC_TFTPNAME :
							(__u8)VEND_RFC_BOOTFILE;
		cp[1] = (__u8)(slen & 0xff);
	}
	overload |= (area == BOOTP_AREA_SNAME) ? VEND_OVR_SNAME : VEND_OVR_FILE;
  }

  /*
   * We found a suitable storage area which has enough space, so append the
   * new tag.
   */
  start = endtags[area];
  *start++ = (__u8)(id & 0xff);
  *start++ = (__u8)(size & 0xff);
  cp = start + (size & 0xff);
  *cp = (__u8)VEND_RFC_END;
  endtags[area] = cp;
  return(start);
}



/*
 *****************************************************************************
 *
 * Set a BOOTP tag value
 */
__INTERNAL(putbootp, short,
			short tag,
			__nptr s1ptr, size_t s1size,
			short isstring)
{
  __u8 *bp;
  char *s1 = (char *)((int)s1ptr);
  size_t blen, slen, i;

  /* Check for valid tag */
  if (tag <= VEND_RFC_NOP ||
      tag == VEND_RFC_END ||
      tag == VEND_PXE_END ||
      tag == VEND_RFC_VENDOR ||
      tag == VEND_RFC_OVERLOAD)
	return(0);

  /* Determine how much space we need */
  slen = isstring ? s1size : (size_t)(s1[0]);

  /* Delete the old BOOTP tag if it exists */
  bp = find_tag(BOOTP_AREA_ANY, tag, &blen);
  if (tag > VEND_RFC_END) {
	/*
	 * We cant delete or add non-RFC tags. Therefore, "slen" must not
	 * be zero, and the tag length must match the length of the new
	 * contents.
	 */
	if (slen == 0 || blen == 0 || blen != slen)
		return(0);
  } else if (blen != slen) {
	delete_tag(bp);
	blen = 0;
  }

  /* Add a new tag, and store the new contents into it */
  if (slen > 0) {
	if (blen == 0) {
		bp = add_tag(BOOTP_AREA_ANY, tag, slen);
		if (bp == NULL)
			return(0);
	}
	for (i = 0; i < (slen - 1); i++)
		bp[i] = (isstring ? (__u8)pc2iso(s1[i + 1]) : (__u8)(s1[i]));
  }
  return(0);
}


/* Function wrapper */
__PUBLIC(putbootp, 4,
		__A(short, 3),
		__A(__nptr, 1), __A(size_t, 2),
		__A(short, 0)
);


/* Export info */
__EXPORT_PROC(putbootp, NONE, 3, __ARGS(
		__D(NUM, NONE),
		__D(ANY, REF),
		__D(NUM, NONE)));



/*
 *****************************************************************************
 *
 * Return a BOOTP tag value as a string
 */
__INTERNAL(getbootp, __nptr,
		__nptr retptr, size_t retsize, short tag, short isstring)
{
  __u8 *bp = NULL;
  char *ret = (char *)((int)retptr);
  size_t blen, i;

  /* Find BOOTP tag */
  blen = 0;
  if (tag > VEND_RFC_NOP &&
      tag != VEND_RFC_END &&
      tag != VEND_PXE_END &&
      tag != VEND_NB_END &&
      tag != VEND_RFC_VENDOR &&
      tag != VEND_RFC_OVERLOAD)
	bp = find_tag(BOOTP_AREA_ANY, tag, &blen);
  if (blen == 0 || (!isstring && blen < retsize)) {
	for (i = 0; i < retsize; i++)
		ret[i] = (char)0;
	return(retptr);
  }

  /* Convert BOOTP tag into output string */
  if (isstring) {
	if (blen > (retsize - 1))
		blen = retsize - 1;
	for (i = 0; i < blen && bp[i] != (__u8)0; i++)
		ret[i + 1] = (char)iso2pc(bp[i]);
	ret[0] = (char)(i & 0xff);
  } else {
	for (i = 0; i < blen; i++)
		ret[i] = (char)(bp[i]);
  }
  return(retptr);
}


/* Function wrapper */
__PUBLIC(getbootp, 4,
		__A(__nptr, 2), __A(size_t, 3),
		__A(short, 1),
		__A(short, 0)
);


/* Export info */
__EXPORT_PROC(getbootp, ANY, 2, __ARGS(
		__D(NUM, NONE),
		__D(NUM, NONE)));



/*
 *****************************************************************************
 *
 * Export some BOOTP constants and variables
 */
__EXPORT_CONST(BOOTP_HOSTNAME, NUM, VEND_RFC_HNAME);
__EXPORT_CONST(BOOTP_DOMAIN, NUM, VEND_RFC_DOMAIN);
__EXPORT_CONST(BOOTP_ROOTPATH, NUM, VEND_RFC_ROOTPATH);

__EXPORT_VARCONST(servername, STRING, BOOTP_SNAME_SIZE);
__EXPORT_VAR(bootfile, STRING, BOOTP_FILE_SIZE);
__EXPORT_VAR(hostname, STRING, MAX_HOST_NAME);
__EXPORT_VAR(domainname, STRING, MAX_DOMAIN_NAME);
__EXPORT_VAR(tftpaddr, IPADDR, 0);
__EXPORT_VAR(gatewayaddr, IPADDR, 0);
__EXPORT_VAR(dnsaddr, IPADDR, 0);



/*
 *****************************************************************************
 *
 * Terminate BOOTP record handling
 */
static short exit_bootp(__u16 retval)
{
  __u8 *bp;
  size_t blen, slen, i;

  /* Call any other daisy-chained exit routines first */
  retval = callnext(nextexit, retval);

  /* If we return with error, don't update any BOOTP record fields */
  if (retval != RTERROR_LOAD && retval != RTERROR_RUN)
	return(retval);

  /* Update boot server address */
  bp = find_tag(BOOTP_AREA_ANY, VEND_NB_TFTPSERVER, &blen);
  if (blen == 0)
	bootp->bp_siaddr = tftpaddr;
  else if (blen >= IP_ALEN)
	*((t_ipaddr *)bp) = tftpaddr;

  /* Update gateway address */
  bp = find_tag(BOOTP_AREA_ANY, VEND_RFC_ROUTER, &blen);
  if (gatewayaddr != htonl(IP_ANY)) {
	if (blen > 0 && blen < IP_ALEN) {
		delete_tag(bp);
		blen = 0;
	}
	if (blen == 0) {
		bp = add_tag(BOOTP_AREA_ANY, VEND_RFC_ROUTER, IP_ALEN);
		if (bp == NULL)
			rtint_error("Unable to save gateway info");
	}
	*((t_ipaddr *)bp) = gatewayaddr;
  } else if (blen > 0)
	delete_tag(bp);

  /* Update DNS server address */
  bp = find_tag(BOOTP_AREA_ANY, VEND_RFC_DNS, &blen);
  if (dnsaddr != htonl(IP_ANY)) {
	if (blen > 0 && blen < IP_ALEN) {
		delete_tag(bp);
		blen = 0;
	}
	if (blen == 0) {
		bp = add_tag(BOOTP_AREA_ANY, VEND_RFC_DNS, IP_ALEN);
		if (bp == NULL)
			rtint_error("Unable to save DNS server info");
	}
	*((t_ipaddr *)bp) = dnsaddr;
  } else if (blen > 0)
	delete_tag(bp);

  /* Update boot file name */
  if (overload != VEND_OVR_FILE && overload != VEND_OVR_BOTH)
	for (i = 0; i < BOOTP_FILE_SIZE; i++)
		bootp->bp_file[i] = 0;
  slen = (size_t)(bootfile[0]);
  if (slen > (BOOTP_FILE_SIZE - 1))
	slen = BOOTP_FILE_SIZE - 1;
  bp = find_tag(BOOTP_AREA_ANY, VEND_RFC_BOOTFILE, &blen);
  if (blen > 0 && (slen == 0 || blen != (slen + 1))) {
	delete_tag(bp);
	blen = 0;
  }
  if (slen > 0) {
	if (blen == 0) {
		bp = bootp->bp_file;
		if (overload == VEND_OVR_FILE || overload == VEND_OVR_BOTH) {
			bp = add_tag(BOOTP_AREA_ANY, VEND_RFC_BOOTFILE, slen + 1);
			if (bp == NULL)
				rtint_error("Unable to save boot file info");
		}
	}
	for (i = 0; i < slen; i++)
		bp[i] = (__u8)pc2iso(bootfile[i + 1]);
	bp[i] = 0;
  }

  /* Care for the overload flag */
  bp = find_tag(BOOTP_AREA_VEND, VEND_RFC_OVERLOAD, &blen);
  if (blen < 1)
	rtint_error("Invalid BOOTP/DHCP record");
  if (overload > 0)
	*bp = overload;
  else
	delete_tag(bp);

  /* Give the BOOTP record back to the bootrom */
  if (canchange && btpptr != FNULL)
	movemem(far2linear(MK_FP(getds(), (int)(bootpmem))),
			far2linear(btpptr),
			btplen);

  /* Now we can safely load the new operating system */
  return(retval);
}



/*
 *****************************************************************************
 *
 * Initialize BOOTP record handling
 */
void init_bootp(void)
{
  __u8 *bp;
  size_t blen, i;

  /* Define the string "none" as ISO-8859-1 characters */
  static __u8 none_str[] = { 0x6e, 0x6f, 0x6e, 0x65, 0 };

  /* Initialize exit routine */
  nextexit = atexit;
  atexit = (__nptr)&exit_bootp;

  /* Copy the BOOTP record from the bootrom data area into our own memory */
  if (btpptr != FNULL && btplen > 0) {
	movemem(far2linear(btpptr),
			far2linear(MK_FP(getds(), (int)(bootpmem))),
			btplen);
	if (bootp->bp_op != BOOTP_OP_REPLY ||
	    *((__u32 *)(bootp->bp_vend)) != BOOTP_VM_RFC1048)
		rtint_error("Got invalid BOOTP/DHCP record from bootrom");
  } else
	rtint_error("Unable to load BOOTP/DHCP record");

  /*
   * If we are unable to return a changed BOOTP record, we are also
   * unable to return with state RTERROR_LOAD.
   */
  if (!canchange)
	canload = FALSE;

  /* Get overload flag and determine amount of free space */
  bp = find_tag(BOOTP_AREA_VEND, VEND_RFC_OVERLOAD, &blen);
  if (blen > 0) {
	overload = *bp;
	find_endtags();
  } else {
	/* Keep space for overload tag */
	overload = 0;
	find_endtags();
	bp = add_tag(BOOTP_AREA_VEND, VEND_RFC_OVERLOAD, 1);
	if (bp == NULL)
		rtint_error("Unable to initialize BOOTP/DHCP record");
	*bp = (__u8)0;
  }

  /* Initialize global IP address variables */
  bp = find_tag(BOOTP_AREA_ANY, VEND_NB_TFTPSERVER, &blen);
  if (blen >= IP_ALEN)
	tftpaddr = *((t_ipaddr *)bp);
  else
	tftpaddr = bootp->bp_siaddr;
  bp = find_tag(BOOTP_AREA_ANY, VEND_RFC_ROUTER, &blen);
  if (blen >= IP_ALEN)
	gatewayaddr = *((t_ipaddr *)bp);
  else
	gatewayaddr = htonl(IP_ANY);
  bp = find_tag(BOOTP_AREA_ANY, VEND_RFC_DNS, &blen);
  if (blen >= IP_ALEN)
	dnsaddr = *((t_ipaddr *)bp);
  else
	dnsaddr = htonl(IP_ANY);

  /* Initialize server name */
  bp = find_tag(BOOTP_AREA_ANY, VEND_RFC_TFTPNAME, &blen);
  if (blen == 0) {
	bp = bootp->bp_sname;
	if (overload == VEND_OVR_SNAME || overload == VEND_OVR_BOTH)
		blen = 0;
	else
		blen = BOOTP_SNAME_SIZE;
  }
  i = 0;
  while (i < blen && *bp)
	servername[++i] = (char)iso2pc(*bp++);
  servername[0] = (char)(i & 0xff);

  /* Initialize host name */
  bp = find_tag(BOOTP_AREA_ANY, VEND_RFC_HNAME, &blen);
  if (blen == 0) {
	bp = none_str;
	blen = sizeof(none_str) - 1;
  }
  i = 0;
  while (i < blen && *bp)
	hostname[++i] = (char)iso2pc(*bp++);
  hostname[0] = (char)(i & 0xff);

  /* Initialize domain name */
  bp = find_tag(BOOTP_AREA_ANY, VEND_RFC_DOMAIN, &blen);
  i = 0;
  while (i < blen && *bp)
	domainname[++i] = (char)iso2pc(*bp++);
  domainname[0] = (char)(i & 0xff);

  /* Initialize boot file name */
  bp = find_tag(BOOTP_AREA_ANY, VEND_RFC_BOOTFILE, &blen);
  if (blen == 0) {
	bp = bootp->bp_file;
	if (overload == VEND_OVR_FILE || overload == VEND_OVR_BOTH)
		blen = 0;
	else
		blen = BOOTP_FILE_SIZE;
  }
  i = 0;
  while (i < blen && *bp)
	bootfile[++i] = (char)iso2pc(*bp++);
  bootfile[0] = (char)(i & 0xff);

  /* Initialize PXE or netboot tag */
  bp = find_tag(BOOTP_AREA_ANY, VEND_RFC_VENDOR, &blen);
  if (blen > 0) {
	if (blen > VEND_NB_STRLEN &&
	    !strcmp((char *)bp, VEND_NB_STRING)) {
		nbstart = bp + VEND_NB_STRLEN;
		nbend = bp + blen;
	} else if (blen > VEND_PXE_STRLEN &&
	           !strcmp((char *)bp, VEND_PXE_STRING)) {
		pxestart = bp + VEND_PXE_STRLEN;
		pxeend = bp + blen;
	}
  }
}

