#include <errno.h>
#include <assert.h>
#include <realmode.h>
#include <memsizes.h>
#include <basemem_packet.h>
#include <ipxe/uaccess.h>
#include <ipxe/segment.h>
#include <ipxe/init.h>
#include <ipxe/netdevice.h>
#include <ipxe/fakedhcp.h>
#include <ipxe/image.h>
#include <ipxe/features.h>

/** @file
 *
 * NBI image format.
 *
 * The Net Boot Image format is defined by the "Draft Net Boot Image
 * Proposal 0.3" by Jamie Honan, Gero Kuhlmann and Ken Yap.  It is now
 * considered to be a legacy format, but it still included because a
 * large amount of software (e.g. nymph, LTSP) makes use of NBI files.
 *
 * Etherboot does not implement the INT 78 callback interface
 * described by the NBI specification.  For a callback interface on
 * x86 architecture, use PXE.
 *
 */

FEATURE ( FEATURE_IMAGE, "NBI", DHCP_EB_FEATURE_NBI, 1 );

/**
 * An NBI image header
 *
 * Note that the length field uses a peculiar encoding; use the
 * NBI_LENGTH() macro to decode the actual header length.
 *
 */
struct imgheader {
	unsigned long magic;		/**< Magic number (NBI_MAGIC) */
	union {
		unsigned char length;	/**< Nibble-coded header length */
		unsigned long flags;	/**< Image flags */
	};
	segoff_t location;		/**< 16-bit seg:off header location */
	union {
		segoff_t segoff;	/**< 16-bit seg:off entry point */
		unsigned long linear;	/**< 32-bit entry point */
	} execaddr;
} __attribute__ (( packed ));

/** NBI magic number */
#define NBI_MAGIC 0x1B031336UL

/* Interpretation of the "length" fields */
#define NBI_NONVENDOR_LENGTH(len)	( ( (len) & 0x0f ) << 2 )
#define NBI_VENDOR_LENGTH(len)		( ( (len) & 0xf0 ) >> 2 )
#define NBI_LENGTH(len) ( NBI_NONVENDOR_LENGTH(len) + NBI_VENDOR_LENGTH(len) )

/* Interpretation of the "flags" fields */
#define	NBI_PROGRAM_RETURNS(flags)	( (flags) & ( 1 << 8 ) )
#define	NBI_LINEAR_EXEC_ADDR(flags)	( (flags) & ( 1 << 31 ) )

/** NBI header length */
#define NBI_HEADER_LENGTH	512

/**
 * An NBI segment header
 *
 * Note that the length field uses a peculiar encoding; use the
 * NBI_LENGTH() macro to decode the actual header length.
 *
 */
struct segheader {
	unsigned char length;		/**< Nibble-coded header length */
	unsigned char vendortag;	/**< Vendor-defined private tag */
	unsigned char reserved;
	unsigned char flags;		/**< Segment flags */
	unsigned long loadaddr;		/**< Load address */
	unsigned long imglength;	/**< Segment length in NBI file */
	unsigned long memlength;	/**< Segment length in memory */
};

/* Interpretation of the "flags" fields */
#define NBI_LOADADDR_FLAGS(flags)	( (flags) & 0x03 )
#define NBI_LOADADDR_ABS		0x00
#define NBI_LOADADDR_AFTER		0x01
#define NBI_LOADADDR_END		0x02
#define NBI_LOADADDR_BEFORE		0x03
#define NBI_LAST_SEGHEADER(flags)	( (flags) & ( 1 << 2 ) )

/* Define a type for passing info to a loaded program */
struct ebinfo {
	uint8_t  major, minor;  /* Version */
	uint16_t flags;         /* Bit flags */
};

/** Info passed to NBI image */
static struct ebinfo loaderinfo = {
	VERSION_MAJOR, VERSION_MINOR,
	0
};

/**
 * Prepare a segment for an NBI image
 *
 * @v image		NBI image
 * @v offset		Offset within NBI image
 * @v filesz		Length of initialised-data portion of the segment
 * @v memsz		Total length of the segment
 * @v src		Source for initialised data
 * @ret rc		Return status code
 */
static int nbi_prepare_segment ( struct image *image, size_t offset __unused,
				 userptr_t dest, size_t filesz, size_t memsz ){
	int rc;

	if ( ( rc = prep_segment ( dest, filesz, memsz ) ) != 0 ) {
		DBGC ( image, "NBI %p could not prepare segment: %s\n",
		       image, strerror ( rc ) );
		return rc;
	}

	return 0;
}

/**
 * Load a segment for an NBI image
 *
 * @v image		NBI image
 * @v offset		Offset within NBI image
 * @v filesz		Length of initialised-data portion of the segment
 * @v memsz		Total length of the segment
 * @v src		Source for initialised data
 * @ret rc		Return status code
 */
static int nbi_load_segment ( struct image *image, size_t offset,
			      userptr_t dest, size_t filesz,
			      size_t memsz __unused ) {
	memcpy_user ( dest, 0, image->data, offset, filesz );
	return 0;
}

/**
 * Process segments of an NBI image
 *
 * @v image		NBI image
 * @v imgheader		Image header information
 * @v process		Function to call for each segment
 * @ret rc		Return status code
 */
static int nbi_process_segments ( struct image *image,
				  struct imgheader *imgheader,
				  int ( * process ) ( struct image *image,
						      size_t offset,
						      userptr_t dest,
						      size_t filesz,
						      size_t memsz ) ) {
	struct segheader sh;
	size_t offset = 0;
	size_t sh_off;
	userptr_t dest;
	size_t filesz;
	size_t memsz;
	int rc;
	
	/* Copy image header to target location */
	dest = real_to_user ( imgheader->location.segment,
			      imgheader->location.offset );
	filesz = memsz = NBI_HEADER_LENGTH;
	if ( ( rc = process ( image, offset, dest, filesz, memsz ) ) != 0 )
		return rc;
	offset += filesz;

	/* Process segments in turn */
	sh_off = NBI_LENGTH ( imgheader->length );
	do {
		/* Read segment header */
		copy_from_user ( &sh, image->data, sh_off, sizeof ( sh ) );
		if ( sh.length == 0 ) {
			/* Avoid infinite loop? */
			DBGC ( image, "NBI %p invalid segheader length 0\n",
			       image );
			return -ENOEXEC;
		}
		
		/* Calculate segment load address */
		switch ( NBI_LOADADDR_FLAGS ( sh.flags ) ) {
		case NBI_LOADADDR_ABS:
			dest = phys_to_user ( sh.loadaddr );
			break;
		case NBI_LOADADDR_AFTER:
			dest = userptr_add ( dest, memsz + sh.loadaddr );
			break;
		case NBI_LOADADDR_BEFORE:
			dest = userptr_add ( dest, -sh.loadaddr );
			break;
		case NBI_LOADADDR_END:
			/* Not correct according to the spec, but
			 * maintains backwards compatibility with
			 * previous versions of Etherboot.
			 */
			dest = phys_to_user ( ( extmemsize() + 1024 ) * 1024
					      - sh.loadaddr );
			break;
		default:
			/* Cannot be reached */
			assert ( 0 );
		}

		/* Process this segment */
		filesz = sh.imglength;
		memsz = sh.memlength;
		if ( ( offset + filesz ) > image->len ) {
			DBGC ( image, "NBI %p segment outside file\n", image );
			return -ENOEXEC;
		}
		if ( ( rc = process ( image, offset, dest,
				      filesz, memsz ) ) != 0 ) {
			return rc;
		}
		offset += filesz;

		/* Next segheader */
		sh_off += NBI_LENGTH ( sh.length );
		if ( sh_off >= NBI_HEADER_LENGTH ) {
			DBGC ( image, "NBI %p header overflow\n", image );
			return -ENOEXEC;
		}

	} while ( ! NBI_LAST_SEGHEADER ( sh.flags ) );

	if ( offset != image->len ) {
		DBGC ( image, "NBI %p length wrong (file %zd, metadata %zd)\n",
		       image, image->len, offset );
		return -ENOEXEC;
	}

	return 0;
}

/**
 * Boot a 16-bit NBI image
 *
 * @v imgheader		Image header information
 * @ret rc		Return status code, if image returns
 */
static int nbi_boot16 ( struct image *image, struct imgheader *imgheader ) {
	int discard_D, discard_S, discard_b;
	int rc;

	DBGC ( image, "NBI %p executing 16-bit image at %04x:%04x\n", image,
	       imgheader->execaddr.segoff.segment,
	       imgheader->execaddr.segoff.offset );

	__asm__ __volatile__ (
		REAL_CODE ( "pushw %%ds\n\t"	/* far pointer to bootp data */
			    "pushw %%bx\n\t"
			    "pushl %%esi\n\t"	/* location */
			    "pushw %%cs\n\t"	/* lcall execaddr */
			    "call 1f\n\t"
			    "jmp 2f\n\t"
			    "\n1:\n\t"
			    "pushl %%edi\n\t"
			    "lret\n\t"
			    "\n2:\n\t"
			    "addw $8,%%sp\n\t"	/* clean up stack */ )
		: "=a" ( rc ), "=D" ( discard_D ), "=S" ( discard_S ),
		  "=b" ( discard_b )
		: "D" ( imgheader->execaddr.segoff ),
		  "S" ( imgheader->location ),
		  "b" ( __from_data16 ( basemem_packet ) )
		: "ecx", "edx", "ebp" );

	return rc;
}

/**
 * Boot a 32-bit NBI image
 *
 * @v imgheader		Image header information
 * @ret rc		Return status code, if image returns
 */
static int nbi_boot32 ( struct image *image, struct imgheader *imgheader ) {
	int discard_D, discard_S, discard_b;
	int rc;

	DBGC ( image, "NBI %p executing 32-bit image at %lx\n",
	       image, imgheader->execaddr.linear );

	/* Jump to OS with flat physical addressing */
	__asm__ __volatile__ (
		PHYS_CODE ( "pushl %%ebx\n\t" /* bootp data */
			    "pushl %%esi\n\t" /* imgheader */
			    "pushl %%eax\n\t" /* loaderinfo */
			    "call *%%edi\n\t"
			    "addl $12, %%esp\n\t" /* clean up stack */ )
		: "=a" ( rc ), "=D" ( discard_D ), "=S" ( discard_S ),
		  "=b" ( discard_b )
		: "D" ( imgheader->execaddr.linear ),
		  "S" ( ( imgheader->location.segment << 4 ) +
			imgheader->location.offset ),
		  "b" ( virt_to_phys ( basemem_packet ) ),
		  "a" ( virt_to_phys ( &loaderinfo ) )
		: "ecx", "edx", "ebp", "memory" );

	return rc;
}

/**
 * Prepare DHCP parameter block for NBI image
 *
 * @v image		NBI image
 * @ret rc		Return status code
 */
static int nbi_prepare_dhcp ( struct image *image ) {
	struct net_device *boot_netdev;
	int rc;

	boot_netdev = last_opened_netdev();
	if ( ! boot_netdev ) {
		DBGC ( image, "NBI %p could not identify a network device\n",
		       image );
		return -ENODEV;
	}

	if ( ( rc = create_fakedhcpack ( boot_netdev, basemem_packet,
					 sizeof ( basemem_packet ) ) ) != 0 ) {
		DBGC ( image, "NBI %p failed to build DHCP packet\n", image );
		return rc;
	}

	return 0;
}

/**
 * Execute a loaded NBI image
 *
 * @v image		NBI image
 * @ret rc		Return status code
 */
static int nbi_exec ( struct image *image ) {
	struct imgheader imgheader;
	int may_return;
	int rc;

	/* Retrieve image header */
	copy_from_user ( &imgheader, image->data, 0, sizeof ( imgheader ) );

	DBGC ( image, "NBI %p placing header at %hx:%hx\n", image,
	       imgheader.location.segment, imgheader.location.offset );

	/* NBI files can have overlaps between segments; the bss of
	 * one segment may overlap the initialised data of another.  I
	 * assume this is a design flaw, but there are images out
	 * there that we need to work with.  We therefore do two
	 * passes: first to initialise the segments, then to copy the
	 * data.  This avoids zeroing out already-copied data.
	 */
	if ( ( rc = nbi_process_segments ( image, &imgheader,
					   nbi_prepare_segment ) ) != 0 )
		return rc;
	if ( ( rc = nbi_process_segments ( image, &imgheader,
					   nbi_load_segment ) ) != 0 )
		return rc;

	/* Prepare DHCP option block */
	if ( ( rc = nbi_prepare_dhcp ( image ) ) != 0 )
		return rc;

	/* Shut down now if NBI image will not return */
	may_return = NBI_PROGRAM_RETURNS ( imgheader.flags );
	if ( ! may_return )
		shutdown_boot();

	/* Execute NBI image */
	if ( NBI_LINEAR_EXEC_ADDR ( imgheader.flags ) ) {
		rc = nbi_boot32 ( image, &imgheader );
	} else {
	        rc = nbi_boot16 ( image, &imgheader );
	}

	if ( ! may_return ) {
		/* Cannot continue after shutdown() called */
		DBGC ( image, "NBI %p returned %d from non-returnable image\n",
		       image, rc  );
		while ( 1 ) {}
	}

	DBGC ( image, "NBI %p returned %d\n", image, rc );

	return rc;
}

/**
 * Probe NBI image
 *
 * @v image		NBI image
 * @ret rc		Return status code
 */
static int nbi_probe ( struct image *image ) {
	struct imgheader imgheader;

	/* If we don't have enough data give up */
	if ( image->len < NBI_HEADER_LENGTH ) {
		DBGC ( image, "NBI %p too short for an NBI image\n", image );
		return -ENOEXEC;
	}

	/* Check image header */
	copy_from_user ( &imgheader, image->data, 0, sizeof ( imgheader ) );
	if ( imgheader.magic != NBI_MAGIC ) {
		DBGC ( image, "NBI %p has no NBI signature\n", image );
		return -ENOEXEC;
	}

	return 0;
}

/** NBI image type */
struct image_type nbi_image_type __image_type ( PROBE_NORMAL ) = {
	.name = "NBI",
	.probe = nbi_probe,
	.exec = nbi_exec,
};
