/*
 *  nandwrite.c
 *
 *  Copyright (C) 2000 Steven J. Hill (sjhill@realitydiluted.com)
 *		  2003 Thomas Gleixner (tglx@linutronix.de)
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * Overview:
 *   This utility writes a binary image directly to a NAND flash
 *   chip or NAND chips contained in DoC devices. This is the
 *   "inverse operation" of nanddump.
 *
 * tglx: Major rewrite to handle bad blocks, write data with or without ECC
 *	 write oob data only on request
 *
 * Bug/ToDo:
 */

#define PROGRAM_NAME "nandwrite"

#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <getopt.h>

#include <asm/types.h>
#include "mtd/mtd-user.h"
#include "common.h"
#include <libmtd.h>

static void display_help(int status)
{
	fprintf(status == EXIT_SUCCESS ? stdout : stderr,
"Usage: nandwrite [OPTION] MTD_DEVICE [INPUTFILE|-]\n"
"Writes to the specified MTD device.\n"
"\n"
"  -a, --autoplace         Use auto OOB layout\n"
"  -k, --skip-all-ffs      Skip pages that contain only 0xff bytes\n"
"  -m, --markbad           Mark blocks bad if write fails\n"
"  -n, --noecc             Write without ecc\n"
"  -N, --noskipbad         Write without bad block skipping\n"
"  -o, --oob               Input contains oob data\n"
"  -O, --onlyoob           Input contains oob data and only write the oob part\n"
"  -s addr, --start=addr   Set output start address (default is 0)\n"
"  --skip-bad-blocks-to-start"
"                          Skip bad blocks when seeking to the start address\n"
"  -p, --pad               Pad writes to page size\n"
"  -b, --blockalign=1|2|4  Set multiple of eraseblocks to align to\n"
"      --input-skip=length Skip |length| bytes of the input file\n"
"      --input-size=length Only read |length| bytes of the input file\n"
"  -q, --quiet             Don't display progress messages\n"
"  -h, --help              Display this help and exit\n"
"  -V, --version           Output version information and exit\n"
	);
	exit(status);
}

static void display_version(void)
{
	common_print_version();
	printf("Copyright (C) 2003 Thomas Gleixner \n"
			"\n"
			"%1$s comes with NO WARRANTY\n"
			"to the extent permitted by law.\n"
			"\n"
			"You may redistribute copies of %1$s\n"
			"under the terms of the GNU General Public Licence.\n"
			"See the file `COPYING' for more information.\n",
			PROGRAM_NAME);
	exit(EXIT_SUCCESS);
}

static const char	*standard_input = "-";
static const char	*mtd_device, *img;
static long long	mtdoffset = 0;
static long long	inputskip = 0;
static long long	inputsize = 0;
static bool		quiet = false;
static bool		writeoob = false;
static bool		onlyoob = false;
static bool		markbad = false;
static bool		noecc = false;
static bool		autoplace = false;
static bool		skipallffs = false;
static bool		noskipbad = false;
static bool		pad = false;
static bool		skip_bad_blocks_to_start = false;
static int		blockalign = 1; /* default to using actual block size */

static void process_options(int argc, char * const argv[])
{
	int error = 0;

	for (;;) {
		int option_index = 0;
		static const char short_options[] = "hb:mnNoOpqs:akV";
		static const struct option long_options[] = {
			/* Order of these args with val==0 matters; see option_index. */
			{"version", no_argument, 0, 'V'},
			{"input-skip", required_argument, 0, 0},
			{"input-size", required_argument, 0, 0},
			{"skip-bad-blocks-to-start", no_argument, 0, 0},
			{"help", no_argument, 0, 'h'},
			{"blockalign", required_argument, 0, 'b'},
			{"markbad", no_argument, 0, 'm'},
			{"noecc", no_argument, 0, 'n'},
			{"noskipbad", no_argument, 0, 'N'},
			{"oob", no_argument, 0, 'o'},
			{"onlyoob", no_argument, 0, 'O'},
			{"pad", no_argument, 0, 'p'},
			{"quiet", no_argument, 0, 'q'},
			{"start", required_argument, 0, 's'},
			{"autoplace", no_argument, 0, 'a'},
			{"skip-all-ffs", no_argument, 0, 'k'},
			{0, 0, 0, 0},
		};

		int c = getopt_long(argc, argv, short_options,
				long_options, &option_index);
		if (c == EOF)
			break;

		switch (c) {
		case 0:
			switch (option_index) {
			case 1: /* --input-skip */
				inputskip = simple_strtoll(optarg, &error);
				break;
			case 2: /* --input-size */
				inputsize = simple_strtoll(optarg, &error);
				break;
			case 3: /* --skip-bad-blocks-to-start */
				skip_bad_blocks_to_start = true;
				break;
			}
			break;
		case 'V':
			display_version();
			break;
		case 'q':
			quiet = true;
			break;
		case 'n':
			noecc = true;
			break;
		case 'N':
			noskipbad = true;
			break;
		case 'm':
			markbad = true;
			break;
		case 'o':
			writeoob = true;
			break;
		case 'O':
			writeoob = true;
			onlyoob = true;
			break;
		case 'p':
			pad = true;
			break;
		case 's':
			mtdoffset = simple_strtoll(optarg, &error);
			break;
		case 'b':
			blockalign = atoi(optarg);
			break;
		case 'a':
			autoplace = true;
			break;
		case 'k':
			skipallffs = true;
			break;
		case 'h':
			display_help(EXIT_SUCCESS);
			break;
		case '?':
			error++;
			break;
		}
	}

	if (mtdoffset < 0)
		errmsg_die("Can't specify negative device offset with option"
				" -s: %lld", mtdoffset);

	if (blockalign <= 0)
		errmsg_die("Can't specify negative or zero blockalign with "
				"option -b: %d", blockalign);

	if (!is_power_of_2(blockalign))
		errmsg_die("Can't specify a non-power-of-two blockalign with "
				"option -b: %d", blockalign);

	if (autoplace && noecc)
		errmsg_die("Autoplacement and no-ECC are mutually exclusive");

	if (!onlyoob && (pad && writeoob))
		errmsg_die("Can't pad when oob data is present");

	argc -= optind;
	argv += optind;

	/*
	 * There must be at least the MTD device node positional
	 * argument remaining and, optionally, the input file.
	 */

	if (argc < 1 || argc > 2 || error)
		display_help(EXIT_FAILURE);

	mtd_device = argv[0];

	/*
	 * Standard input may be specified either explictly as "-" or
	 * implicity by simply omitting the second of the two
	 * positional arguments.
	 */

	img = ((argc == 2) ? argv[1] : standard_input);
}

static void erase_buffer(void *buffer, size_t size)
{
	const uint8_t kEraseByte = 0xff;

	if (buffer != NULL && size > 0)
		memset(buffer, kEraseByte, size);
}

static int is_virt_block_bad(struct mtd_dev_info *mtd, int fd,
				long long offset)
{
	int i, ret = 0;

	for (i = 0; i < blockalign; ++i) {
		ret = mtd_is_bad(mtd, fd, offset / mtd->eb_size + i);
		if (ret)
			break;
	}

	return ret;
}

/*
 * Main program
 */
int main(int argc, char * const argv[])
{
	int fd = -1;
	int ifd = -1;
	int pagelen;
	long long imglen = 0;
	long long blockstart = -1;
	struct mtd_dev_info mtd;
	int ret;
	bool failed = true;
	/* contains all the data read from the file so far for the current eraseblock */
	unsigned char *filebuf = NULL;
	size_t filebuf_max = 0;
	size_t filebuf_len = 0;
	/* points to the current page inside filebuf */
	unsigned char *writebuf = NULL;
	/* points to the OOB for the current page in filebuf */
	unsigned char *oobbuf = NULL;
	libmtd_t mtd_desc;
	int ebsize_aligned;
	uint8_t write_mode;

	process_options(argc, argv);

	/* Open the device */
	if ((fd = open(mtd_device, O_RDWR)) == -1)
		sys_errmsg_die("%s", mtd_device);

	mtd_desc = libmtd_open();
	if (!mtd_desc)
		errmsg_die("can't initialize libmtd");

	/* Fill in MTD device capability structure */
	if (mtd_get_dev_info(mtd_desc, mtd_device, &mtd) < 0)
		errmsg_die("mtd_get_dev_info failed");

	/*
	 * Pretend erasesize is specified number of blocks - to match jffs2
	 *   (virtual) block size
	 * Use this value throughout unless otherwise necessary
	 */
	ebsize_aligned = mtd.eb_size * blockalign;

	if (mtdoffset & (mtd.min_io_size - 1))
		errmsg_die("The start address is not page-aligned !\n"
			   "The pagesize of this NAND Flash is 0x%x.\n",
			   mtd.min_io_size);

	/* Select OOB write mode */
	if (noecc)
		write_mode = MTD_OPS_RAW;
	else if (autoplace)
		write_mode = MTD_OPS_AUTO_OOB;
	else
		write_mode = MTD_OPS_PLACE_OOB;

	if (noecc)  {
		ret = ioctl(fd, MTDFILEMODE, MTD_FILE_MODE_RAW);
		if (ret) {
			switch (errno) {
			case ENOTTY:
				errmsg_die("ioctl MTDFILEMODE is missing");
			default:
				sys_errmsg_die("MTDFILEMODE");
			}
		}
	}

	/* Determine if we are reading from standard input or from a file. */
	if (strcmp(img, standard_input) == 0)
		ifd = STDIN_FILENO;
	else
		ifd = open(img, O_RDONLY);

	if (ifd == -1) {
		perror(img);
		goto closeall;
	}

	pagelen = mtd.min_io_size + ((writeoob) ? mtd.oob_size : 0);

	if (ifd == STDIN_FILENO) {
		imglen = inputsize ? : pagelen;
		if (inputskip) {
			errmsg("seeking stdin not supported");
			goto closeall;
		}
	} else {
		if (!inputsize) {
			struct stat st;
			if (fstat(ifd, &st)) {
				sys_errmsg("unable to stat input image");
				goto closeall;
			}
			imglen = st.st_size - inputskip;
		} else
			imglen = inputsize;

		if (inputskip && lseek(ifd, inputskip, SEEK_CUR) == -1) {
			sys_errmsg("lseek input by %lld failed", inputskip);
			goto closeall;
		}
	}

	/* Check, if file is page-aligned */
	if (!pad && (imglen % pagelen) != 0) {
		fprintf(stderr, "Input file is not page-aligned. Use the padding "
				 "option.\n");
		goto closeall;
	}

	/* Skip bad blocks on the way to the start address if necessary */
	if (skip_bad_blocks_to_start) {
		long long bbs_offset = 0;
		while (bbs_offset < mtdoffset) {
			ret = is_virt_block_bad(&mtd, fd, bbs_offset);
			if (ret < 0) {
				sys_errmsg("%s: MTD get bad block failed", mtd_device);
				goto closeall;
			} else if (ret == 1) {
				if (!quiet)
					fprintf(stderr, "Bad block at %llx, %u block(s) "
						"from %llx will be skipped\n",
						bbs_offset, blockalign, bbs_offset);
				mtdoffset += ebsize_aligned;
			}
			bbs_offset += ebsize_aligned;
		}
	}

	/* Check, if length fits into device */
	if ((imglen / pagelen) * mtd.min_io_size > mtd.size - mtdoffset) {
		fprintf(stderr, "Image %lld bytes, NAND page %d bytes, OOB area %d"
				" bytes, device size %lld bytes\n",
				imglen, pagelen, mtd.oob_size, mtd.size);
		sys_errmsg("Input file does not fit into device");
		goto closeall;
	}

	/*
	 * Allocate a buffer big enough to contain all the data (OOB included)
	 * for one eraseblock. The order of operations here matters; if ebsize
	 * and pagelen are large enough, then "ebsize_aligned * pagelen" could
	 * overflow a 32-bit data type.
	 */
	filebuf_max = ebsize_aligned / mtd.min_io_size * pagelen;
	filebuf = xmalloc(filebuf_max);
	erase_buffer(filebuf, filebuf_max);

	/*
	 * Get data from input and write to the device while there is
	 * still input to read and we are still within the device
	 * bounds. Note that in the case of standard input, the input
	 * length is simply a quasi-boolean flag whose values are page
	 * length or zero.
	 */
	while ((imglen > 0 || writebuf < filebuf + filebuf_len)
		&& mtdoffset < mtd.size) {
		/*
		 * New eraseblock, check for bad block(s)
		 * Stay in the loop to be sure that, if mtdoffset changes because
		 * of a bad block, the next block that will be written to
		 * is also checked. Thus, we avoid errors if the block(s) after the
		 * skipped block(s) is also bad (number of blocks depending on
		 * the blockalign).
		 */
		while (blockstart != (mtdoffset & (~ebsize_aligned + 1))) {
			blockstart = mtdoffset & (~ebsize_aligned + 1);

			/*
			 * if writebuf == filebuf, we are rewinding so we must
			 * not reset the buffer but just replay it
			 */
			if (writebuf != filebuf) {
				erase_buffer(filebuf, filebuf_len);
				filebuf_len = 0;
				writebuf = filebuf;
			}

			if (!quiet)
				fprintf(stdout, "Writing data to block %lld at offset 0x%llx\n",
						 blockstart / ebsize_aligned, blockstart);

			if (noskipbad)
				continue;

			ret = is_virt_block_bad(&mtd, fd, blockstart);

			if (ret < 0) {
				sys_errmsg("%s: MTD get bad block failed", mtd_device);
				goto closeall;
			} else if (ret == 1) {
				if (!quiet)
					fprintf(stderr,
						"Bad block at %llx, %u block(s) "
						"will be skipped\n",
						blockstart, blockalign);

				mtdoffset = blockstart + ebsize_aligned;

				if (mtdoffset > mtd.size) {
					errmsg("too many bad blocks, cannot complete request");
					goto closeall;
				}
			}
		}

		/* Read more data from the input if there isn't enough in the buffer */
		if (writebuf + mtd.min_io_size > filebuf + filebuf_len) {
			size_t readlen = mtd.min_io_size;
			size_t alreadyread = (filebuf + filebuf_len) - writebuf;
			size_t tinycnt = alreadyread;
			ssize_t cnt = 0;

			while (tinycnt < readlen) {
				cnt = read(ifd, writebuf + tinycnt, readlen - tinycnt);
				if (cnt == 0) { /* EOF */
					break;
				} else if (cnt < 0) {
					perror("File I/O error on input");
					goto closeall;
				}
				tinycnt += cnt;
			}

			/* No padding needed - we are done */
			if (tinycnt == 0) {
				/*
				 * For standard input, set imglen to 0 to signal
				 * the end of the "file". For nonstandard input,
				 * leave it as-is to detect an early EOF.
				 */
				if (ifd == STDIN_FILENO)
					imglen = 0;

				break;
			}

			/* Padding */
			if (tinycnt < readlen) {
				if (!pad) {
					fprintf(stderr, "Unexpected EOF. Expecting at least "
							"%zu more bytes. Use the padding option.\n",
							readlen - tinycnt);
					goto closeall;
				}
				erase_buffer(writebuf + tinycnt, readlen - tinycnt);
			}

			filebuf_len += readlen - alreadyread;
			if (ifd != STDIN_FILENO) {
				imglen -= tinycnt - alreadyread;
			} else if (cnt == 0) {
				/* No more bytes - we are done after writing the remaining bytes */
				imglen = 0;
			}
		}

		if (writeoob) {
			oobbuf = writebuf + mtd.min_io_size;

			/* Read more data for the OOB from the input if there isn't enough in the buffer */
			if (oobbuf + mtd.oob_size > filebuf + filebuf_len) {
				size_t readlen = mtd.oob_size;
				size_t alreadyread = (filebuf + filebuf_len) - oobbuf;
				size_t tinycnt = alreadyread;
				ssize_t cnt;

				while (tinycnt < readlen) {
					cnt = read(ifd, oobbuf + tinycnt, readlen - tinycnt);
					if (cnt == 0) { /* EOF */
						break;
					} else if (cnt < 0) {
						perror("File I/O error on input");
						goto closeall;
					}
					tinycnt += cnt;
				}

				if (tinycnt < readlen) {
					fprintf(stderr, "Unexpected EOF. Expecting at least "
							"%zu more bytes for OOB\n", readlen - tinycnt);
					goto closeall;
				}

				filebuf_len += readlen - alreadyread;
				if (ifd != STDIN_FILENO) {
					imglen -= tinycnt - alreadyread;
				} else if (cnt == 0) {
					/* No more bytes - we are done after writing the remaining bytes */
					imglen = 0;
				}
			}
		}

		ret = 0;
		if (!skipallffs || !buffer_check_pattern(writebuf, mtd.min_io_size, 0xff)) {
			/* Write out data */
			ret = mtd_write(mtd_desc, &mtd, fd, mtdoffset / mtd.eb_size,
					mtdoffset % mtd.eb_size,
					onlyoob ? NULL : writebuf,
					onlyoob ? 0 : mtd.min_io_size,
					writeoob ? oobbuf : NULL,
					writeoob ? mtd.oob_size : 0,
					write_mode);
		}

		if (ret) {
			if (errno != EIO) {
				sys_errmsg("%s: MTD write failure", mtd_device);
				goto closeall;
			}

			/* Must rewind to blockstart if we can */
			writebuf = filebuf;

			fprintf(stderr, "Erasing failed write from %#08llx to %#08llx\n",
				blockstart, blockstart + ebsize_aligned - 1);

			if (mtd_erase_multi(mtd_desc, &mtd, fd,
					blockstart / mtd.eb_size, blockalign)) {
				int errno_tmp = errno;
				sys_errmsg("%s: MTD Erase failure", mtd_device);
				if (errno_tmp != EIO)
					goto closeall;
			}

			if (markbad) {
				fprintf(stderr, "Marking block at %08llx bad\n",
						mtdoffset & (~mtd.eb_size + 1));
				if (mtd_mark_bad(&mtd, fd, mtdoffset / mtd.eb_size)) {
					sys_errmsg("%s: MTD Mark bad block failure", mtd_device);
					goto closeall;
				}
			}
			mtdoffset = blockstart + ebsize_aligned;

			continue;
		}
		mtdoffset += mtd.min_io_size;
		writebuf += pagelen;
	}

	failed = false;

closeall:
	close(ifd);
	libmtd_close(mtd_desc);
	free(filebuf);
	close(fd);

	if (failed || (ifd != STDIN_FILENO && imglen > 0)
		   || (writebuf < filebuf + filebuf_len))
		sys_errmsg_die("Data was only partially written due to error");

	/* Return happy */
	return EXIT_SUCCESS;
}
