/*
 * rfddump.c
 *
 * Copyright (C) 2005 Sean Young <sean@mess.org>
 *
 *  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
 *  (at your option) any later version.
 */

#define _XOPEN_SOURCE 500 /* For pread */

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <getopt.h>

#include <mtd/mtd-user.h>
#include <linux/types.h>
#include <mtd_swab.h>

/* next is an array of mapping for each corresponding sector */
#define RFD_MAGIC		0x9193
#define HEADER_MAP_OFFSET       3
#define SECTOR_DELETED          0x0000
#define SECTOR_ZERO             0xfffe
#define SECTOR_FREE             0xffff

#define SECTOR_SIZE             512

#define SECTORS_PER_TRACK	63


struct rfd {
	int block_size;
	int block_count;
	int header_sectors;
	int data_sectors;
	int header_size;
	uint16_t *header;
	int sector_count;
	int *sector_map;
	const char *mtd_filename;
	const char *out_filename;
	int verbose;
};

#define PROGRAM "rfddump"
#define VERSION "$Revision 1.0 $"

void display_help(void)
{
	printf("Usage: " PROGRAM " [OPTIONS] MTD-device filename\n"
			"Dumps the contents of a resident flash disk\n"
			"\n"
			"-h         --help               display this help and exit\n"
			"-V         --version            output version information and exit\n"
			"-v         --verbose		Be verbose\n"
			"-b size    --blocksize          Block size (defaults to erase unit)\n");
	exit(0);
}

void display_version(void)
{
	printf(PROGRAM " " VERSION "\n"
			"\n"
			"This is free software; see the source for copying conditions.  There is NO\n"
			"warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n");

	exit(0);
}

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

	rfd->block_size = 0;
	rfd->verbose = 0;

	for (;;) {
		int option_index = 0;
		static const char *short_options = "hvVb:";
		static const struct option long_options[] = {
			{ "help", no_argument, 0, 'h' },
			{ "version", no_argument, 0, 'V', },
			{ "blocksize", required_argument, 0, 'b' },
			{ "verbose", no_argument, 0, 'v' },
			{ NULL, 0, 0, 0 }
		};

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

		switch (c) {
			case 'h':
				display_help();
				break;
			case 'V':
				display_version();
				break;
			case 'v':
				rfd->verbose = 1;
				break;
			case 'b':
				rfd->block_size = atoi(optarg);
				break;
			case '?':
				error = 1;
				break;
		}
	}

	if ((argc - optind) != 2 || error)
		display_help();

	rfd->mtd_filename = argv[optind];
	rfd->out_filename = argv[optind + 1];
}

int build_block_map(struct rfd *rfd, int fd, int block)
{
	int  i;
	int sectors;

	if (pread(fd, rfd->header, rfd->header_size, block * rfd->block_size)
			!= rfd->header_size) {
		return -1;
	}

	if (le16_to_cpu(rfd->header[0]) != RFD_MAGIC) {
		if (rfd->verbose)
			printf("Block #%02d: Magic missing\n", block);

		return 0;
	}

	sectors =  0;
	for (i=0; i<rfd->data_sectors; i++) {
		uint16_t entry = le16_to_cpu(rfd->header[i + HEADER_MAP_OFFSET]);

		if (entry == SECTOR_FREE || entry == SECTOR_DELETED)
			continue;

		if (entry == SECTOR_ZERO)
			entry = 0;

		if (entry >= rfd->sector_count) {
			fprintf(stderr, "%s: warning: sector %d out of range\n",
					rfd->mtd_filename, entry);
			continue;
		}

		if (rfd->sector_map[entry] != -1) {
			fprintf(stderr, "%s: warning: more than one entry "
					"for sector %d\n", rfd->mtd_filename, entry);
			continue;
		}

		rfd->sector_map[entry] = rfd->block_size * block +
			(i + rfd->header_sectors) * SECTOR_SIZE;
		sectors++;
	}

	if (rfd->verbose)
		printf("Block #%02d: %d sectors\n", block, sectors);

	return 1;
}

int main(int argc, char *argv[])
{
	int fd, sectors_per_block;
	mtd_info_t mtd_info;
	struct rfd rfd;
	int i, blocks_found;
	int out_fd = 0;
	uint8_t sector[512];
	int blank, rc, cylinders;

	process_options(argc, argv, &rfd);

	fd = open(rfd.mtd_filename, O_RDONLY);
	if (fd == -1) {
		perror(rfd.mtd_filename);
		return 1;
	}

	if (rfd.block_size == 0) {
		if (ioctl(fd, MEMGETINFO, &mtd_info)) {
			perror(rfd.mtd_filename);
			close(fd);
			return 1;
		}

		if (mtd_info.type != MTD_NORFLASH) {
			fprintf(stderr, "%s: wrong type\n", rfd.mtd_filename);
			close(fd);
			return 2;
		}

		sectors_per_block = mtd_info.erasesize / SECTOR_SIZE;

		rfd.block_size = mtd_info.erasesize;
		rfd.block_count = mtd_info.size / mtd_info.erasesize;
	} else {
		struct stat st;

		if (fstat(fd, &st) == -1) {
			perror(rfd.mtd_filename);
			close(fd);
			return 1;
		}

		if (st.st_size % SECTOR_SIZE)
			fprintf(stderr, "%s: warning: not a multiple of sectors (512 bytes)\n", rfd.mtd_filename);

		sectors_per_block = rfd.block_size / SECTOR_SIZE;

		if (st.st_size % rfd.block_size)
			fprintf(stderr, "%s: warning: not a multiple of block size\n", rfd.mtd_filename);

		rfd.block_count = st.st_size / rfd.block_size;

		if (!rfd.block_count) {
			fprintf(stderr, "%s: not large enough for one block\n", rfd.mtd_filename);
			close(fd);
			return 2;
		}
	}

	rfd.header_sectors =
		((HEADER_MAP_OFFSET + sectors_per_block) *
		 sizeof(uint16_t) + SECTOR_SIZE - 1) / SECTOR_SIZE;
	rfd.data_sectors = sectors_per_block - rfd.header_sectors;
	cylinders = ((rfd.block_count - 1) * rfd.data_sectors - 1)
		/ SECTORS_PER_TRACK;
	rfd.sector_count = cylinders * SECTORS_PER_TRACK;
	rfd.header_size =
		(HEADER_MAP_OFFSET + rfd.data_sectors) * sizeof(uint16_t);

	rfd.header = malloc(rfd.header_size);
	if (!rfd.header) {
		perror(PROGRAM);
		close(fd);
		return 2;
	}
	rfd.sector_map = malloc(rfd.sector_count * sizeof(int));
	if (!rfd.sector_map) {
		perror(PROGRAM);
		close(fd);
		free(rfd.sector_map);
		return 2;
	}

	rfd.mtd_filename = rfd.mtd_filename;

	for (i=0; i<rfd.sector_count; i++)
		rfd.sector_map[i] = -1;

	for (blocks_found=i=0; i<rfd.block_count; i++) {
		rc = build_block_map(&rfd, fd, i);
		if (rc > 0)
			blocks_found++;
		if (rc < 0)
			goto err;
	}

	if (!blocks_found) {
		fprintf(stderr, "%s: no RFD blocks found\n", rfd.mtd_filename);
		goto err;
	}

	for (i=0; i<rfd.sector_count; i++) {
		if (rfd.sector_map[i] != -1)
			break;
	}

	if (i == rfd.sector_count) {
		fprintf(stderr, "%s: no sectors found\n", rfd.mtd_filename);
		goto err;
	}

	out_fd = open(rfd.out_filename, O_WRONLY | O_TRUNC | O_CREAT, 0666);
	if (out_fd == -1) {
		perror(rfd.out_filename);
		goto err;
	}

	blank = 0;
	for (i=0; i<rfd.sector_count; i++) {
		if (rfd.sector_map[i] == -1) {
			memset(sector, 0, SECTOR_SIZE);
			blank++;
		} else {
			if (pread(fd, sector, SECTOR_SIZE, rfd.sector_map[i])
					!= SECTOR_SIZE) {
				perror(rfd.mtd_filename);
				goto err;
			}
		}

		if (write(out_fd, sector, SECTOR_SIZE) != SECTOR_SIZE) {
			perror(rfd.out_filename);
			goto err;
		}
	}

	if (rfd.verbose)
		printf("Copied %d sectors (%d blank)\n", rfd.sector_count, blank);

	close(out_fd);
	close(fd);
	free(rfd.header);
	free(rfd.sector_map);

	return 0;

err:
	if (out_fd)
		close(out_fd);

	close(fd);
	free(rfd.header);
	free(rfd.sector_map);

	return 2;
}