/*
 * mknbi.c  -  MaKe NetBoot Image for Menu Generation Language
 *
 * Copyright (C) 1997-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: mknbi.c,v 1.22 2007/01/06 18:31:28 gkminix Exp $
 */

#define NEED_BINARY 1
#include "mknbi.h"
#include "mgl.h"
#include <system/nbloader.h>
#include <dos/exec.h>

#ifndef MKNBI_H_MGL
#error Included wrong header file
#endif



/*
 * Forward declarations
 */
static void proc_outtype __P((struct cmdopt *opt, char *arg));



/*
 * Copyright information
 */
static const char *copyright[] = {
	COPYRIGHT,
	NULL
};





/*
 * Local and global variables
 */
static struct sysdef sysdata;		/* System data definition */
static char *batchname = NULL;		/* Name of system to batch process */
static char *savename = NULL;		/* Name of system to use for save */
static char *definclude = NULL;		/* Default include file path */
static int deftype = OUTPUT_DEFAULT;	/* Default output file type */
static int outfile;			/* File handle for output file */
static int rtfile;			/* File handle for runtime module */
static int ldfile;			/* File handle for runtime loader */

int maxerrors = MAXERRS;		/* Maximum number of errors */
int printsizes = FALSE;			/* Print binary file sizes to stdout */

static int cur_rec_num = -1;		/* Number of current load record */
static struct load_header header;	/* Load header */
static struct load_record *cur_rec;	/* Pointer to current load record */
static __u8 copyrec_buf[SECTSIZE];	/* Copy buffer */



/*
 * Values for output file type command line option
 */
static struct enumdef otypes[] = {
	{ "plain",	OUTPUT_PLAIN },
	{ "netboot",	OUTPUT_NETBOOT },
	{ "pxe",	OUTPUT_PXE },
	{ "dos",	OUTPUT_DOS },
	{ NULL,		OUTPUT_USER }
};



/*
 * Definition of mknbi-mgl:general section in configuration file
 */
static struct paramdef genparams[] = {
  { "incpath",		par_string,	NULL,	{&definclude}},
  { "outtype",		par_enum,	otypes,	{&deftype}},
  { "maxerrors",	par_int,	NULL,	{&maxerrors}},
  { NULL,		par_null,	NULL,	{NULL}}
};



/*
 * List of sections to read from configuration files at startup
 */
static struct sectdef confsects[] = {
	{ "mknbi-mgl:general",	genparams,	NULL,	NULL },
	{ NULL,			NULL,		NULL,	NULL }
};



/*
 * Command line options and arguments
 */
static struct cmdopt opts[] = {
	{ NULL, 0, copyrightmsg, {copyright}, NULL, NULL		},
	{ "batch-sys", 'b', strval, {&batchname},
	  "name of system to process", "SYSTEM"				},
	{ "save-sys", 'S', strval, {&savename},
	  "name of system for saving into database", "SYSTEM"		},
	{ "includes", 'I', strval, {&sysdata.incpath},
	  "include file path", "PATH"					},
	{ "infile", 'i', strval, {&sysdata.mglname},
	  "name of MGL source file", "FILE"				},
	{ "outfile", 'o', strval, {&sysdata.outname},
	  "name of boot image output file", "FILE"			},
	{ "runtime", 'r', strval, {&sysdata.rtname},
	  "name of MGL runtime library file", "FILE"			},
	{ "loader", 'l', strval, {&sysdata.ldname},
	  "name of primary loader file", "FILE"				},
	{ "filetype", 't', procval, {(voidstar)&proc_outtype},
	  "type of output file:\n"
	  "  STRING syntax: plain|netboot|pxe|dos", "STRING"		},
	{ "print-sizes", '\0', boolval, {&printsizes},
	  NULL, NULL							},
	{ "infile", 0, nonopt, {&sysdata.mglname},
	  "name of MGL source (if not given as option)", NULL		},
	{ "outfile", 0, nonopt, {&sysdata.outname},
	  "output file (if not given as option)", NULL			},
	{ NULL, 0, noval, {NULL}, NULL, NULL				}
};



/*
 * Write a buffer into the output file and update the load record
 */
static void putrec __F((recnum, src, size),
				int recnum AND
				__u8 *src AND
				size_t size)
{
  unsigned long l;

  assert(cur_rec_num == recnum);
  (void)nbwrite(src, size, outfile);
  l = get_long(cur_rec->ilength) + size;
  assign(cur_rec->ilength.low, htot(low_word(l)));
  assign(cur_rec->ilength.high, htot(high_word(l)));
  l = get_long(cur_rec->mlength) + size;
  assign(cur_rec->mlength.low, htot(low_word(l)));
  assign(cur_rec->mlength.high, htot(high_word(l)));
}



/*
 * Copy the compiled image file into the output file
 */
static void copyrec __F((recnum, image), int recnum AND int image)
{
  size_t i, j = 0;

  /* Copy data in SECTSIZE chunks */
  while ((i = nbread(copyrec_buf, SECTSIZE, image)) > 0) {
	putrec(recnum, copyrec_buf, i);
	j = i;
  }

  /* Roundup the last chunk to a full SECTSIZE */
  if (j > 0 && j < SECTSIZE) {
	memzero(&(copyrec_buf[j]), SECTSIZE - j);
	putrec(recnum, &(copyrec_buf[j]), SECTSIZE - j);
  }
}



/*
 * Initialize a load record
 */
static void initrec __F((recnum, segment, flags, vendor_size),
					int recnum AND
					unsigned long segment AND
					int flags AND
					int vendor_size)
{
  cur_rec_num++;
  assert(cur_rec_num == recnum);
  if (cur_rec_num > 0)
	cur_rec = (struct load_record *)((__u8 *)cur_rec +
					((cur_rec->rlength << 2) & 0x3c) +
					((cur_rec->rlength >> 2) & 0x3c));
  cur_rec->rlength      = (((sizeof(struct load_record) -
#ifdef USE_VENDOR
		             sizeof(union vendor_data)
#else
		             0
#endif
		                                      ) >> 2) |
                           (((vendor_size + 3) & 0x3c) << 2)) & 0xff;
  cur_rec->rtag1        = (recnum + LOADER_TAG) & 0xff;
  cur_rec->rflags       = flags & 0xff;
  assign(cur_rec->address.low, htot(low_word(segment << 4)));
  assign(cur_rec->address.high, htot(high_word(segment << 4)));
}



/*
 * Process boot loader image
 */
static void do_loader __F_NOARGS
{
  /*
   * For a user defined loader, just copy the loader input file
   * into the output file.
   */
  if (sysdata.outtype == OUTPUT_USER) {
	size_t i;

	assert(ldfile >= 0);
	while ((i = nbread(copyrec_buf, SECTSIZE, ldfile)) > 0)
		(void)nbwrite(copyrec_buf, i, outfile);
	return;
  }

  /*
   * For a netboot boot image loader, determine the relevant segment
   * sizes, and check that they are correct. We can use assert() here
   * because an error should never happen. Then copy the loader binary
   * into the output file while updating the header information.
   */
  if (sysdata.outtype == OUTPUT_NETBOOT) {
	__u8 *dataptr;
	unsigned long tsize, dsize, ssize, msize;
	size_t lsize;
	struct nbld_header *hp;

	/* Determine the segment sizes */
	lsize = roundup(first_nb_data_size, SECTSIZE);
	dataptr = (__u8 *)nbmalloc(lsize);
	memcpy(dataptr, first_nb_data, first_nb_data_size);
	hp = (struct nbld_header *)&(dataptr[NBLD_OFFSET]);
	tsize = get_word(hp->textsize);
	dsize = get_word(hp->datasize);
	ssize = get_word(hp->heapsize) + get_word(hp->stacksize);
	msize = roundup(tsize, 16) + roundup((dsize + ssize), 16) + 1;

	/* Check that they are correct */
	assert(lsize <= BOOTLLSIZE);
	assert(msize <= BOOTLMSIZE && msize >= lsize);

	/* Copy the loader binary into the output file */
	initrec(BOOTLNUM, DEF_BOOTLSEG, 0, 0);
	putrec(BOOTLNUM, dataptr, lsize);
	assign(cur_rec->mlength.low, htot(low_word(msize)));
	assign(cur_rec->mlength.high, htot(high_word(msize)));

	/* Set the image entry address correctly */
	assign(header.execute.segment, htot(DEF_BOOTLSEG));
	assign(header.execute.offset, getval(hp->nbsetup));
	free(dataptr);
	return;
  }

  /*
   * For the DOS output file type, we put the name of the output file
   * into the binary. This is necessary for some debuggers which don't
   * setup the DOS environment correctly. However this will only be
   * done for output file names which are likely to be the final DOS
   * executable name. Otherwise, the game is lost...
   */
  if (sysdata.outtype == OUTPUT_DOS) {
	__u8 *dataptr, *scan;
	size_t hsize, lsize, len;
	struct exehdr *hp;
	char *cp;
	int i;

	/* Determine base name of output file */
	if ((cp = strrchr(sysdata.outname, '/')) == NULL)
		cp = sysdata.outname;
	len = (cp == NULL ? 0 : strlen(cp));
	if (len < 5 || len > 12 ||
	    (strcmp(&(cp[len - 4]), ".exe") != 0 &&
	     strcmp(&(cp[len - 4]), ".EXE") != 0))
		cp = NULL;

	/* Check for valid EXE file header */
	dataptr = first_dos_data;
	hp = (struct exehdr *)dataptr;
	lsize = first_dos_data_size;
	hsize = 0;
	if (lsize > sizeof(struct exehdr))
		hsize = get_word(hp->headsize) * 16;
	scan = NULL;
	if (lsize > (hsize + 16)) {
		scan = dataptr + hsize + 2;
		for (i = 0; i < 14; i++)
			if (scan[i] != 0) {
				scan = NULL;
				break;
			}
	}

	/* Copy the output file name into the binary */
	if (cp != NULL && scan != NULL)
		bytecpy(cp, scan, bytelen(cp));

	/* Write the binary into the output file */
	(void)nbwrite(dataptr, lsize, outfile);
	return;
  }

  /*
   * For PXE output file type, we can just copy the loader binary without
   * updating any header.
   */
  if (sysdata.outtype == OUTPUT_PXE) {
	__u8 *dataptr;
	size_t lsize;

	dataptr = first_pxe_data;
	lsize = first_pxe_data_size;
	(void)nbwrite(dataptr, lsize, outfile);
	return;
  }

  /* For plain output file type we can just return */
  return;
}



/*
 * Compile the MGL program
 */
static void do_compile __F_NOARGS
{
  int tmpfile;

  /* Initialize the netboot load record */
  if (sysdata.outtype == OUTPUT_NETBOOT)
	initrec(MGLNUM, DEF_PROGSEG, BOOT_FLAG_EOF, 0);

  /* Compile the program directly into the output file */
  if (sysdata.outtype == OUTPUT_PLAIN) {
	tmpfile = compile(sysdata.mglname, sysdata.rtname,
					sysdata.outname, sysdata.incpath);
	close(tmpfile);
	return;
  }

  /* Compile the program into a temporary file */
  tmpfile = compile(sysdata.mglname, sysdata.rtname, NULL, sysdata.incpath);

  /* Copy the temporary file into the output file */
  if (sysdata.outtype == OUTPUT_NETBOOT) {
	copyrec(MGLNUM, tmpfile);
	assign(cur_rec->mlength.low, htot(low_word(PROGMSIZE)));
	assign(cur_rec->mlength.high, htot(high_word(PROGMSIZE)));
  } else {
	size_t i;

	while ((i = nbread(copyrec_buf, SECTSIZE, tmpfile)) > 0)
		(void)nbwrite(copyrec_buf, i, outfile);
  }
  closetemp(tmpfile);
}



/*
 * Dump the load record information to stderr for netboot output file type
 */
static void dump_header __F((lh), const struct load_header *lh)
{
  static const char *s_tags[] = { /* BOOTLNUM */   "primary boot loader",
				  /* MGLNUM */     "compiled program"};
  static const char *s_flags[]= { "absolute address",
				  "after previous segment",
				  "at end of memory",
				  "before previous segment"};

  struct load_record *lr;
  char *vendstr = NULL;
  int i, num = 0;

  if (logfd(LOGLEVEL_INFO) == NULL)
	return;

  vendstr = bytestr(lh->dummy, (lh->hlength >> 2) & 0x3c);
  prnlog(LOGLEVEL_INFO,
	  "\nLoad record information:\n"
	  "  Magic number:     0x%08lX\n"
	  "  Length of header: %d bytes (standard) + %d bytes (vendor)\n"
	  "  Flags:            0x%08lX\n"
	  "  Location address: %04X:%04X\n"
	  "  Execute address:  %04X:%04X\n"
	  "  Vendor data:      %s\n"
	  "\n",
	  get_long(lh->magic),
	  (lh->hlength << 2) & 0x3c,
	  (lh->hlength >> 2) & 0x3c,
	  (unsigned long)lh->hflags1 +
		((unsigned long)lh->hflags2 << 8) +
		((unsigned long)lh->hflags3 << 16),
	  get_word(lh->locn.segment), get_word(lh->locn.offset),
	  get_word(lh->execute.segment), get_word(lh->execute.offset),
	  vendstr);

  i  = ((lh->hlength >> 2) & 0x3c) + ((lh->hlength << 2) & 0x3c);
  lr = (struct load_record *)&(((__u8 *)lh)[i]);

  while (TRUE) {
  	prnlog(LOGLEVEL_INFO,
	    "\nRecord #%d:\n"
	    "  Length of header: %d bytes (standard) + %d bytes (vendor)\n"
	    "  Vendor tag:       0x%02X (%s)\n"
	    "  Reserved flags:   0x%02X\n"
	    "  Flags:            0x%02X (%s%s)\n"
	    "  Load address:     0x%08lX%s\n"
	    "  Image length:     0x%08lX bytes\n"
	    "  Memory length:    0x%08lX bytes\n",
	    ++num,
	    (lr->rlength << 2) & 0x3c,
	    (lr->rlength >> 2) & 0x3c,
	    (int)lr->rtag1,
	    (lr->rtag1 < (__u8)LOADER_TAG) ||
		(lr->rtag1 - (__u8)LOADER_TAG >= (__u8)NUM_RECORDS) ?
		"unknown" : s_tags[(unsigned)(lr->rtag1 - LOADER_TAG)],
	    (int)lr->rtag2,
	    (int)lr->rflags, s_flags[(unsigned)(lr->rflags & 0x03)],
	    lr->rflags & BOOT_FLAG_EOF ? ", last record" : "",
	    get_long(lr->address),
	    get_long(lr->address) >= 0x100000L ? " (high memory)" : "",
	    get_long(lr->ilength),
	    get_long(lr->mlength));

	if (lr->rflags & BOOT_FLAG_EOF)
		break;

	i  = ((lr->rlength >> 2) & 0x3c) + ((lr->rlength << 2) & 0x3c);
	lr = (struct load_record *)&(((__u8 *)lr)[i]);
  }
  free(vendstr);
}



/*
 * Process output file type command line option
 */
static void proc_outtype __F((opt, arg), struct cmdopt *opt AND char *arg)
{
  int i;

  if (arg != NULL) {
	for (i = 0; otypes[i].enumstr != NULL; i++)
		if (!strcmp(otypes[i].enumstr, arg)) {
			sysdata.outtype = otypes[i].val;
			return;
		}
	prnerr("invalid output file type argument \"%s\"", arg);
	nbexit(EXIT_USAGE);
  }
}



/*
 * Main program
 */
int main __F((argc, argv), int argc AND char **argv)
{
  /* Initialize parameters */
  memzero(&sysdata, sizeof(sysdata));
  sysdata.outtype = OUTPUT_USER;

  /* Parse options and read configuration file */
  nbsetup(argc, argv, opts, confsects);
  if (batchname != NULL) {
	if (savename != NULL) {
		prnerr("-b and -S options cannot get used together");
		nbexit(EXIT_USAGE);
	}
	getdb(batchname, &sysdata);
  }
  if (sysdata.outname == NULL) {
	prnerr("need output file name");
	nbexit(EXIT_USAGE);
  }
  if (sysdata.outtype != OUTPUT_USER && sysdata.ldname != NULL) {
	prnerr("unable to use filetype and loader arguments together");
	nbexit(EXIT_USAGE);
  } else if (sysdata.outtype == OUTPUT_USER && sysdata.ldname == NULL) {
	/*
	 * Set a default output file type if no runtime loader file and no
	 * output file type has been given.
	 */
	sysdata.outtype = deftype;
  }

  /*
   * Set the default include file path, if no path given on the
   * command line.
   */
  if (sysdata.incpath == NULL && definclude != NULL)
	copystr(&sysdata.incpath, definclude);

  /*
   * The --print-sizes option is undocumented, and doesn't even appear
   * on the runtime help output. It gets used by the test routines of
   * the netboot source code distribution to generate assembly output
   * files. This option automatically reduces the verbosity value to 0
   * so that no output will be produced besides the file size values.
   */
  if (printsizes)
	verbose = LOGLEVEL_NORMAL;

  /*
   * Open the loader, runtime and output files. The runtime module
   * file gets only opened for testing purposes. We close it right
   * after opening, because the compiler routine opens it again
   * lateron.
   */
  rtfile = -1;
  if (sysdata.rtname != NULL &&
      (rtfile = open(sysdata.rtname, O_RDONLY | O_BINARY)) < 0) {
	prnerr("unable to open runtime module file %s", sysdata.rtname);
	nbexit(EXIT_MGL_RUNTIME);
  }
  ldfile = -1;
  if (sysdata.ldname != NULL &&
      (ldfile = open(sysdata.ldname, O_RDONLY | O_BINARY)) < 0) {
	prnerr("unable to open runtime loader file %s", sysdata.ldname);
	nbexit(EXIT_MGL_LOADER);
  }
  outfile = -1;
  if (sysdata.outtype != OUTPUT_PLAIN &&
      (outfile = creat(sysdata.outname, 0644)) < 0) {
	prnerr("unable to create output file %s", sysdata.outname);
	nbexit(EXIT_MGL_IMGCREATE);
  }

  prnlog(LOGLEVEL_NOTICE, "Source file name         = %s\n",
		(sysdata.mglname == NULL ? "<stdin>" : sysdata.mglname));
  if (sysdata.incpath != NULL)
		prnlog(LOGLEVEL_NOTICE, "Include file path        = %s\n",
							sysdata.incpath);
  if (sysdata.ldname != NULL)
		prnlog(LOGLEVEL_NOTICE, "Runtime loader file name = %s\n",
							sysdata.ldname);
  if (sysdata.rtname != NULL)
		prnlog(LOGLEVEL_NOTICE, "Runtime module file name = %s\n",
							sysdata.rtname);
  prnlog(LOGLEVEL_NOTICE, "Output file name         = %s\n",
							sysdata.outname);

  if (rtfile >= 0) {
	close(rtfile);
	rtfile = -1;
  }

  /* Initialize the boot header */
  cur_rec = NULL;
  if (sysdata.outtype == OUTPUT_NETBOOT) {
	memzero(&header, sizeof(header));
	assign(header.magic.low,       htot(low_word(NBHEADER_MAGIC)));
	assign(header.magic.high,      htot(high_word(NBHEADER_MAGIC)));
	assign(header.locn.segment,    htot(DEF_HEADERSEG));
	assign(header.locn.offset,     htot(0));
	assign(header.bootsig,         htot(NBHEADER_SIG));
	header.hlength  = ((__u8)((int)(header.dummy - (__u8 *)&header) /
				sizeof(__u32)) & 0x0f) |
				((__u8)(VENDOR_SIZE << 4) & 0xf0);
	header.hflags1  = FLAG1_KEEP_ALL;
	bytecpy(VENDOR_MAGIC, header.dummy, VENDOR_SIZE * sizeof(__u32));
	(void)nbwrite((__u8 *)&header, sizeof(header), outfile);
	cur_rec = (struct load_record *)&(header.dummy[VENDOR_SIZE * sizeof(__u32)]);
  }

  /* Process all load records. The order of these calls is important! */
  do_loader();
  do_compile();

  /* After writing out all this stuff, finally update the boot header */
  if (sysdata.outtype == OUTPUT_NETBOOT) {
	if (lseek(outfile, 0, SEEK_SET) != 0) {
		prnerr("unable to seek to beginning of output file");
		nbexit(EXIT_SEEK);
	}
	(void)nbwrite((__u8 *)&header, sizeof(header), outfile);
  }

  /* If user asked for detailed output, parse the header and output all of */
  /* the load record information */
  if (sysdata.outtype == OUTPUT_NETBOOT)
	dump_header(&header);
  prnlog(LOGLEVEL_NOTICE, "Wrote image file \"%s\"\n", sysdata.outname);

  /* Write system definition into systems database */
  if (savename != NULL) {
	putdb(savename, &sysdata, definclude);
	prnlog(LOGLEVEL_NOTICE, "Wrote database entry \"%s\"\n\n", savename);
  }
  nbexit(EXIT_SUCCESS);
#if !defined(__GNUC__) || __GNUC__ < 3
  /* Make the compiler happy, GCC knows that nbexit() never returns */
  return(0);
#endif
}

