/* vim: set filetype=c:
 *
 **************************************************************************
 *
 * Boot-ROM-Code to load an operating system across a TCP/IP network.
 *
 * Module:  base.c86
 * Purpose: Perform the actual booting process
 * Entries: do_boot
 *
 **************************************************************************
 *
 * 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: base.c86,v 1.14 2007/01/06 18:30:46 gkminix Exp $
 */


#include <common.h86>
#include <romconfig.h86>
#include <i386/string.h86>
#include <i386/misc.h86>
#include <i386/conio.h86>
#include <i386/setjmp.h86>
#include <net/in.h86>
#include <net/bootp.h86>
#include <pxe/general.h86>
#include <pxe/entry.h86>
#include <pxe/romid.h86>
#include <pxe/base.h86>
#include "headers/net.h86"
#include "headers/pxe.h86"
#include "headers/arpa.h86"
#include "headers/funcs.h86"
#include "load.h86"
#include "base.h86"



/*
 **************************************************************************
 * 
 * Specifies which part of the bootrom code to terminate
 */
#define TERM_ALL	0		/* terminate everything */
#define TERM_BASE	1		/* terminate the kernel only */
#define TERM_UNDI	2		/* terminate the UNDI driver only */



/*
 **************************************************************************
 * 
 * Variables local to this module
 */
static jmp_buf restartenv;		/* setjmp buffer for restart	*/
static struct loadparams ldparams;	/* load parameters		*/
static __u16 oldfbm;			/* FBM before base code loaded	*/
static __u16 startfbm;			/* FBM when base code loaded	*/
static __u16 undiseg;			/* UNDI start segment		*/
static __u16 undisize;			/* total size of UNDI		*/
static __u16 undistack;			/* stack size of UNDI		*/




/*
 **************************************************************************
 * 
 * Find the start and size of the UNDI driver
 */
static void findundi(void)
{
  __u16 endseg;
  union {
    t_pxe_struct pxe;
    t_undi_romid romid;
  } s;
  __fptr structptr = MK_FP(getds(), &s);
  __fptr romidptr;

#define descaddr(x)	s.pxe.desc[(x)].addr
#define descsize(x)	s.pxe.desc[(x)].size

  /* Determine lowest address occupied by UNDI, and compute it's size */
  fmemcpy(structptr, pxeptr, sizeof(t_pxe_struct));
  romidptr = s.pxe.undiromid;
  undiseg = 0;
  endseg = 0;
  if (descaddr(PXE_DESC_UNDI_CODE) < descaddr(PXE_DESC_UNDI_DATA)) {
	undiseg = (__u16)((descaddr(PXE_DESC_UNDI_CODE) >> 4) & 0xFFFF);
	endseg = (__u16)(((descaddr(PXE_DESC_UNDI_DATA) +
			descsize(PXE_DESC_UNDI_DATA) + 0x000F) >> 4) & 0xFFFF);
  } else {
	undiseg = (__u16)((descaddr(PXE_DESC_UNDI_DATA) >> 4) & 0xFFFF);
	endseg = (__u16)(((descaddr(PXE_DESC_UNDI_CODE) +
			descsize(PXE_DESC_UNDI_CODE) + 0x000F) >> 4) & 0xFFFF);
  }
  undisize = endseg - undiseg;

  /* Determine stack size required for UNDI */
  fmemcpy(structptr, romidptr, sizeof(t_undi_romid));
  undistack = s.romid.stacksize;

#undef descaddr
#undef descsize
}



/*
 **************************************************************************
 * 
 * Unload the bootrom kernel services from memory. According to the PXE
 * spec this routine just clears all relevant fields in the !PXE and
 * PXENV structures. It is up to the caller to restore the base of free
 * memory.
 */
static short unload_stack(void)
{
  union {
    t_pxenv_entry pxenv;
    t_pxe_struct  pxe;
  } s;
  __fptr structptr = MK_FP(getds(), &s);
  __u16 stackseg = 0;
  __u16 stacksize = 0;

  /* Check if base of free memory has been changed */
  if (startfbm == 0)
	return(PXENV_STATUS_UNLOAD_BASE);
  if (startfbm != getfbm())
	return(PXENV_STATUS_KEEP_ALL);

  /* Compute stack segment for UNDI as the new base */
  if (undistack > 0) {
	stackseg = undiseg - (undistack >> 4) - 1;
	stacksize = undistack;
  }

  /* Remove the base code from the PXENV structure */
  fmemcpy(structptr, pxenvptr, sizeof(t_pxenv_entry));
  s.pxenv.bc_stk_sel = stackseg;
  s.pxenv.bc_stk_size = stacksize;
  s.pxenv.bc_cs_sel = 0;
  s.pxenv.bc_cs_size = 0;
  s.pxenv.bc_ds_sel = 0;
  s.pxenv.bc_ds_size = 0;
  s.pxenv.checksum -= dochecksum((__u8 *)&(s.pxenv), sizeof(t_pxenv_entry));
  fmemcpy(pxenvptr, structptr, sizeof(t_pxenv_entry));

  /* Remove the base code from the !PXE structure */
  if (pxeptr != FNULL) {
	fmemcpy(structptr, pxeptr, sizeof(t_pxe_struct));
	s.pxe.baseromid = 0;
	s.pxe.desc[PXE_DESC_STACK].segment = stackseg;
	s.pxe.desc[PXE_DESC_STACK].addr = (__u32)stackseg << 4;
	s.pxe.desc[PXE_DESC_STACK].size = stacksize;
	memset(&(s.pxe.desc[PXE_DESC_BASE_DATA]), 0, 3 * sizeof(t_desc));
	s.pxe.checksum -= dochecksum((__u8 *)&(s.pxe), sizeof(t_pxe_struct));
	fmemcpy(pxeptr, structptr, sizeof(t_pxe_struct));
  }

  /* Set new base of free memory */
  if (stackseg > 0)
	setfbm(stackseg >> 6);
  else
	setfbm(undiseg >> 6);
  startfbm = 0;
  return(PXENV_STATUS_SUCCESS);
}



/*
 **************************************************************************
 * 
 * Terminate the bootrom services according to the wishes of the loaded
 * boot image. This routine never gets called directly by a PXE function.
 */
static short terminate(short mode)
{
  register short retbase, retundi;

  /* Terminate the kernel */
  retbase = PXENV_STATUS_SUCCESS;
  if (mode == TERM_ALL || mode == TERM_BASE) {
	if (net_stop() != PXENV_STATUS_SUCCESS)
		retbase = PXENV_STATUS_STOP_BASE;
	else if (unload_stack() != PXENV_STATUS_SUCCESS)
		retbase = PXENV_STATUS_UNLOAD_BASE;
  }

  /* Terminate the UNDI driver */
  retundi = PXENV_STATUS_SUCCESS;
  if (mode == TERM_ALL || mode == TERM_UNDI) {
	retundi = net_cleanup();
	if (retbase == PXENV_STATUS_SUCCESS &&
	    retundi == PXENV_STATUS_SUCCESS)
		setfbm(oldfbm);
  }

  /* Check for proper return code */
  if (retbase != PXENV_STATUS_SUCCESS)
	return(retbase);
  else if (retundi != PXENV_STATUS_SUCCESS)
	return(retundi);
  return(PXENV_STATUS_SUCCESS);
}



/*
 **************************************************************************
 * 
 * Call the loaded image
 *
 * This routine returns FALSE, if the bootrom should return to the BIOS.
 * In this case, the reason for the error is given in romret. If romret
 * is PXENV_STATUS_SUCCESS, everything has been removed from memory. Other-
 * wise some part of the bootrom is still in memory.
 */
static bool_t execute(short *romret)
{
  short mode = ldparams.mode;
  bool_t ldret;

  /* Terminate all parts which are not required */
  *romret = PXENV_STATUS_SUCCESS;
  if (mode == LDMODE_KEEP_NONE)
	*romret = terminate(TERM_ALL);
  else if (mode == LDMODE_KEEP_UNDI)
	*romret = terminate(TERM_BASE);
  if (*romret != PXENV_STATUS_SUCCESS)
	return(FALSE);

  /* Call the boot image */
  ldret = exec_image(mode, ldparams.exec,
	 	               ldparams.header,
	 	               bootp_bufs[BOOTP_ACK]);
  if (!ldret || mode == LDMODE_KEEP_PXE) {
	/* The image wants us to return to the BIOS */
	if (mode == LDMODE_KEEP_UNDI)
		*romret = terminate(TERM_UNDI);
	else if (mode == LDMODE_KEEP_ALL ||
	         mode == LDMODE_KEEP_PXE)
		*romret = terminate(TERM_ALL);
	return(FALSE);
  }

  /* If anything has been removed already, we can't continue */
  if (mode == LDMODE_KEEP_NONE) {
	*romret = PXENV_STATUS_SUCCESS;
	return(FALSE);
  } else if (mode == LDMODE_KEEP_UNDI) {
	*romret = terminate(TERM_UNDI);
	return(FALSE);
  }
  return(TRUE);
}



/*
 **************************************************************************
 * 
 * Print an IP number. Note that the IP address should be in network
 * order.
 */
static void printip(t_ipaddr ip)
{
  register __u8 *cp = (__u8 *)&ip;

  printf("%u.%u.%u.%u", cp[0], cp[1], cp[2], cp[3]);
}



/*
 **************************************************************************
 * 
 * Set parameters from BOOTP record
 */
static short setparams(void)
{
  register __u8 *cp;
  __u8 *sname, *hname;
  size_t snamlen, hnamlen;
  size_t len;
  short overload = 0;

  cur_bootp_buf = BOOTP_ACK;
  memset(&ldparams, 0, sizeof(ldparams));

  /* Get the overloading tag */
  (void)get_vend(VEND_RFC_NOP);
  if ((cp = get_vend(VEND_RFC_OVERLOAD)) != NULL && *cp > 0)
	overload = *(cp + 1);

  /* Determine the IP address of the TFTP server */
  if ((cp = get_vend(VEND_NB_TFTPSERVER)) != NULL && *cp == 4)
	ldparams.tftp.server = *((t_ipaddr *)(cp + 1));
  else
	ldparams.tftp.server = bootp_bufs[BOOTP_ACK]->bp_siaddr;
  if (ldparams.tftp.server == htonl(IP_ANY))
	return(PXENV_STATUS_DHCP_NO_IP_ADDR);

  /* Determine the port number of the TFTP server */
  if ((cp = get_vend(VEND_NB_TFTPSPORT)) != NULL && *cp == 2)
	ldparams.port = ntohs(*((__u16 *)(cp + 1)));

  /* Determine name of server the boot image file will come from */
  sname = NULL;
  snamlen = 0;
  if ((cp = get_vend(VEND_RFC_TFTPNAME)) != NULL && *cp > 0) {
	sname = cp + 1;
	snamlen = *cp;
  } else if (!(overload & VEND_OVR_SNAME)) {
	sname = bootp_bufs[BOOTP_ACK]->bp_sname;
	snamlen = BOOTP_SNAME_SIZE;
  }

  /* Determine our own client host name */
  hname = NULL;
  hnamlen = 0;
  if ((cp = get_vend(VEND_RFC_HNAME)) != NULL && *cp > 0) {
	hname = cp + 1;
	hnamlen = *cp;
  }

  /* Determine name of boot image file to load */
  if (((cp = get_vend(VEND_NB_BOOTFILE)) != NULL && *cp > 0) ||
      ((cp = get_vend(VEND_RFC_BOOTFILE)) != NULL && *cp > 0)) {
	len = *cp;
	if (len > sizeof(ldparams.tftp.filename))
		len = sizeof(ldparams.tftp.filename);
	memcpy(ldparams.tftp.filename, cp + 1, len);
  } else if (!(overload & VEND_OVR_FILE))
	memcpy(ldparams.tftp.filename, bootp_bufs[BOOTP_ACK]->bp_file,
						sizeof(ldparams.tftp.filename));
  if (ldparams.tftp.filename[0] == '\0')
	return(PXENV_STATUS_DHCP_NO_FILE);

  /* Get any router information */
  if ((cp = get_vend(VEND_RFC_ROUTER)) != NULL && *cp >= 4)
	ldparams.tftp.gateway = *((t_ipaddr *)(cp + 1));

  /* Print some of the information we got so far */
  printf("\n\nLocal IP:   "); printip(myipaddr);
  if (hname != NULL)
	printf("  (%ls)", hname, hnamlen);
  printf("\nServer IP:  "); printip(ldparams.tftp.server);
  if (sname != NULL)
	printf("  (%ls)", sname, snamlen);
  if (ldparams.tftp.gateway != htonl(IP_ANY)) {
	printf("\nGateway IP: ");
	printip(ldparams.tftp.gateway);
  }
  printf("\n");
  return(PXENV_STATUS_SUCCESS);
}



/*
 **************************************************************************
 * 
 * Actually do the network boot process. This routine is called START_BASE
 * in the PXE specification, but it only gets called by the ROM loader, and
 * it does terminate UNDI by itself upon return, so that there is no need
 * to make it callable through the PXE interface. It gets called directly
 * by the loader routine.
 */
short do_boot(void *biosparams)
{
  short ret;

  /* Set the base of free memory */
  oldfbm = getfbm();
  startfbm = getds() >> 6;
  setfbm(startfbm);

  /* Find the UNDI address */
  findundi();

  /* Setup the status callout address */
  statuscallout = FNULL;

  /* Startup the network code */
  if ((ret = net_start(biosparams)) != PXENV_STATUS_SUCCESS) {
	setfbm(oldfbm);
	return(ret);
  }

  /* Now start the network booting process */
  if ((ret = bootp()) == PXENV_STATUS_SUCCESS) {
	while ((ret = setparams()) == PXENV_STATUS_SUCCESS) {
		if ((ret = load(&ldparams)) != PXENV_STATUS_SUCCESS)
			break;
		printf("\n\nStarting image...\n");
		setjmp(&restartenv);
		if (!execute(&ret))
			goto boot_ret;
	}
  }

  /* Remove everything from memory and return to BIOS */
  (void)terminate(TERM_ALL);

boot_ret:
  return(ret);
}



/*
 **************************************************************************
 * 
 * Stop the bootrom services
 */
static short pxe_stop_base(t_base_stop_base *params)
{
  return(net_stop());
}



/*
 **************************************************************************
 * 
 * Unload the base code
 */
static short pxe_unload_stack(t_base_unload_stack *params)
{
  return(unload_stack());
}



/*
 **************************************************************************
 * 
 * Restart TFTP PXE function
 *
 * Note that the PXE function dispatcher already cared for setting
 * DS and SS correctly, so that we can jump back into the bootrom
 * code.
 */
static short pxe_restart_tftp(t_base_restart_tftp *params)
{
  register short ret = PXENV_STATUS_FAILURE;

  memset(&ldparams, 0, sizeof(ldparams));
  memcpy(&ldparams.tftp, params, sizeof(ldparams.tftp));
  if ((ret = load(&ldparams)) == PXENV_STATUS_SUCCESS) {
	/*
	 * We have to use longjump here to jump back to the original
	 * bootrom loader code. Otherwise, the bootrom stack will
	 * overflow with multiple calls of this routine.
	 */
	longjmp(&restartenv, 0);
  }
  return(ret);
}



/*
 **************************************************************************
 * 
 * Return BOOTP/DHCP information
 */
static short pxe_get_binl_info(t_base_get_binl_info *params)
{
  register __u8 *cp = NULL;
  register short ret;
  size_t len, limit;

  ret = PXENV_STATUS_FAILURE;
  if (params->packet_type >= BASE_PACKET_DHCP_DISCOVER &&
      params->packet_type <= BASE_PACKET_BINL_REPLY) {
	cur_bootp_buf = params->packet_type - 1;
	cp = (__u8 *)(bootp_bufs[cur_bootp_buf]);
	limit = bootp_sizes[cur_bootp_buf];
	len = get_vend(VEND_RFC_END) - cp;
	if (params->buffer_size == 0 && params->buffer == 0) {
		/* Requesting the buffer size and buffer address */
		params->buffer = MK_FP(getds(), cp);
		params->buffer_size = len;
		params->buffer_limit = limit;
		ret = PXENV_STATUS_SUCCESS;
	} else if (params->buffer_size >= len && params->buffer != 0) {
		/* Requesting to copy buffer */
		fmemcpy(params->buffer, MK_FP(getds(), cp), len);
		params->buffer_size = len;
		params->buffer_limit = limit;
		ret = PXENV_STATUS_SUCCESS;
	}
  }
  return(ret);
}



/*
 **************************************************************************
 *
 * Function table for TFTP PXE functions
 */
struct functab base_func_tab = {
	BASE_MINNUM, BASE_MAXNUM,
	{{ pxe_unload_stack,	sizeof(t_base_unload_stack)},
	 { pxe_get_binl_info,	sizeof(t_base_get_binl_info)},
	 { NULL,		0 },
	 { pxe_restart_tftp,	sizeof(t_base_restart_tftp)},
	 { NULL,		0 },
	 { NULL,		0 },
	 { pxe_stop_base,	sizeof(t_base_stop_base)}
	}
};

