/*
 * unyaffs: extract files from yaffs2 file system image to current directory
 *
 * Created by Kai Wei <kai.wei.cn@gmail.com>
 * Modified by Bernhard Ehlers <be@bernhard-ehlers.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.
 */

/*
 * History:
 * V0.1  2008-12-29
 *   Initial version uploaded to http://code.google.com/p/unyaffs/
 * V0.8  2011-08-25
 *   Fork created at https://github.com/ehlers/unyaffs
 *   Support of chunksizes from 2k to 16k
 *   Restore special files (device nodes)
 *   Restore file date and time
 *   Restore file ownership, when run as root
 *   File listing
 *   Much more error checking
 * V0.9  2011-09-03
 *   Allow - as filename for stdin
 *   Optional base dir for file extraction
 * V0.9.1  2012-04-12
 *   Additional flash layout: 8k chunk size, 448 byte spare
 * V0.9.2  2012-04-18
 *   Additional flash layout: 8k chunk size, 368 byte spare
 *   New options -c and -s to set chunk and spare size, obsoletes option -l
 *   Add support for bad block information in spare header.
 *   Allow first data chunk to be stored in image before file header.
 * V0.9.3  2012-04-30
 *   check result of lchown system call
 *   Code cleanup
 * V0.9.4  2012-05-01
 *   No predefined flash layouts, detect all possible layouts
 *   Option -d shows detected flash layout, no extraction
 * V0.9.5  2012-12-02
 *   Bug in verifying the -s (spare) parameter
 * V0.9.6  2013-04-09
 *   Added man page
 * V0.9.7  2018-03-29
 *   Directory creation doesn't fail, if it already exists
 *   Fix compiler warnings for newer GCC
 */

#define VERSION		"0.9.7"

/* check if lutimes is available */
#if defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__) || (defined(__APPLE__) && defined(__MACH__))
#define HAS_LUTIMES 1
#endif

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <stddef.h>
#include <stdarg.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#if defined(__linux__) || defined(__GLIBC__)
#include <sys/sysmacros.h>
#endif
#ifdef HAS_LUTIMES
#include <sys/time.h>
#else
#include <utime.h>
#endif

#include "unyaffs.h"

#define MIN_CHUNK_SIZE		 2048
#define MAX_CHUNK_SIZE		16384
#define MIN_SPARE_SIZE		   64
#define MAX_SPARE_SIZE		  512
#define HASH_SIZE		 7001
#define MAX_WARN		   20
#define YAFFS_OBJECTID_ROOT	    1

#define STD_PERMS		(S_IRWXU|S_IRWXG|S_IRWXO)
#define EXTRA_PERMS		(S_ISUID|S_ISGID|S_ISVTX)

unsigned char data[MAX_CHUNK_SIZE + MAX_SPARE_SIZE];
unsigned char buffer[4*(MAX_CHUNK_SIZE + MAX_SPARE_SIZE)];
unsigned char *chunk_data = data;
unsigned char *spare_data = NULL;
int chunk_size = MIN_CHUNK_SIZE;
int spare_size = MIN_SPARE_SIZE;
int spare_off  = 0;
int buf_len = 0;
int buf_idx = 0;
int chunk_no   = 0;
int warn_count = 0;
int warn_chown = 0;
int img_file;
int opt_list;
int opt_verbose;

typedef struct _object {
	unsigned id;
	struct _object *next;
	yaffs_ObjectType type;
	unsigned prev_dir_id;
	__u32    atime;
	__u32    mtime;
	char     path_name[1];		/* variable length, must be last */
} object;

object *obj_list[HASH_SIZE];

unsigned last_dir_id;

int set_utime(const char *filename, __u32 yst_atime, __u32 yst_mtime) {
#ifdef HAS_LUTIMES
	struct timeval ftime[2];

	ftime[0].tv_sec  = yst_atime;
	ftime[0].tv_usec = 0;
	ftime[1].tv_sec  = yst_mtime;
	ftime[1].tv_usec = 0;

	return lutimes(filename, ftime);
#else
	struct utimbuf ftime;

	ftime.actime  = yst_atime;
	ftime.modtime = yst_mtime;

	return utime(filename, &ftime);
#endif
}

/* error reporting function, similar to GNU error() */
static void prt_err(int status, int errnum, const char *format, ...) {
	va_list varg;

	va_start(varg, format);
	fflush(stdout);
	vfprintf(stderr, format, varg);
	if (errnum != 0)
		fprintf(stderr, ": %s", strerror(errnum));
	fprintf(stderr, "\n");
	va_end(varg);

	if (status != 0)
		exit(status);
}

/* read function, which handles partial and interrupted reads */
ssize_t safe_read(int fd, void *buf, size_t len) {
	char *ptr = buf;
	ssize_t offset, ret;

	offset = 0;
	while (offset < len) {
		ret = read(fd, ptr+offset, len-offset);
		if (ret < 0) {
			if (errno != EAGAIN && errno != EINTR)
				return -1;
		} else if (ret == 0)
			break;
		else
			offset += ret;
	}
	return offset;
}

/* write function, which handles partial and interrupted writes */
ssize_t safe_write(int fd, void *buf, size_t len) {
	char *ptr = buf;
	ssize_t offset, ret;

	offset = 0;
	while (offset < len) {
		ret = write(fd, ptr+offset, len-offset);
		if (ret < 0) {
			if (errno != EAGAIN && errno != EINTR)
				return -1;
		} else if (ret == 0)
			break;
		else
			offset += ret;
	}
	return offset;
}

/*
 * mkdirpath - creates directories including intermediate dirs
 */
static int mkdirpath(const char *name, mode_t mode) {
	struct stat st;
	char *cp;
	char *buf;

	if ((buf = malloc(strlen(name)+1)) == NULL)
		{ errno = ENOMEM; return -1; }
	strcpy(buf, name);
	cp = buf;
	while ((cp = strchr(cp, '/')) != NULL) {
		*cp = '\0';
		mkdir(buf, 0755);
		*cp++ = '/';
	}
	free(buf);

	if (mkdir(name, mode) < 0) {
		if (stat(name, &st) < 0 || !S_ISDIR(st.st_mode))
			return -1;
		chmod(name, mode);
	}

	return 0;
}

/*
 * save_lchown - call lchown and check result
 */
static void safe_lchown(const char *path, uid_t owner, gid_t group) {
	if (lchown(path, owner, group) < 0) {
		if (errno == EPERM || errno == EINVAL)
			warn_chown = 1;
		else
			prt_err(1, errno, "Can't chown %s", path);
	}
}

static void init_obj_list(void) {
	object *obj;
	unsigned idx;

	for (idx = 0; idx < HASH_SIZE; idx++)
		obj_list[idx] = NULL;
	last_dir_id = 0;

	obj = malloc(offsetof(object, path_name) + 2);
	if (obj == NULL)
		prt_err(1, 0, "Malloc struct object failed.");

	obj->id = YAFFS_OBJECTID_ROOT;
	obj->type = YAFFS_OBJECT_TYPE_DIRECTORY;
	obj->prev_dir_id = 0;
	obj->atime = obj->mtime = 0;
	strcpy(obj->path_name, ".");
	idx = obj->id % HASH_SIZE;
	obj->next = obj_list[idx];
	obj_list[idx] = obj;
}

static object *get_object(unsigned id) {
	object *obj;

	obj = obj_list[id % HASH_SIZE];
	while (obj != NULL && obj->id != id)
		obj = obj->next;
	return obj;
}

static object *add_object(yaffs_ObjectHeader *oh, yaffs_PackedTags2 *pt) {
	object *obj, *parent;
	unsigned idx;

	obj = get_object(pt->t.objectId);
	if (pt->t.objectId == YAFFS_OBJECTID_ROOT) {
		if (obj == NULL)
			prt_err(1, 0, "Missing root object");
		if (oh->type != YAFFS_OBJECT_TYPE_DIRECTORY)
			prt_err(1, 0, "Root object must be directory");
		if (last_dir_id == 0)
			last_dir_id = YAFFS_OBJECTID_ROOT;
	} else {
		if (oh->type != YAFFS_OBJECT_TYPE_FILE &&
		    oh->type != YAFFS_OBJECT_TYPE_DIRECTORY &&
		    oh->type != YAFFS_OBJECT_TYPE_SYMLINK &&
		    oh->type != YAFFS_OBJECT_TYPE_HARDLINK &&
		    oh->type != YAFFS_OBJECT_TYPE_SPECIAL &&
		    oh->type != YAFFS_OBJECT_TYPE_UNKNOWN)
			prt_err(1, 0, "Illegal type %d in object %u (%s)",
			        oh->type, pt->t.objectId, oh->name);
		if (oh->name[0] == '\0' || strchr(oh->name, '/') != NULL ||
		    strcmp(oh->name, ".") == 0 || strcmp(oh->name, "..") == 0)
			prt_err(1, 0, "Illegal file name %s in object %u",
			        oh->name, pt->t.objectId);
		if (obj != NULL)
			prt_err(1, 0, "Duplicate objectId %u", pt->t.objectId);
		parent = get_object(oh->parentObjectId);
		if (parent == NULL)
			prt_err(1, 0, "Invalid parentObjectId %u in object %u (%s)",
			        oh->parentObjectId, pt->t.objectId, oh->name);
		if (parent->type != YAFFS_OBJECT_TYPE_DIRECTORY)
			prt_err(1, ENOTDIR, "File %s can't be created in %s",
			        oh->name, parent->path_name);
		obj = malloc(offsetof(object, path_name) +
		             strlen(parent->path_name) + strlen(oh->name) + 2);
		if (obj == NULL)
			prt_err(1, 0, "Malloc struct object failed.");

		obj->id = pt->t.objectId;
		obj->type = oh->type;
		if (obj->type == YAFFS_OBJECT_TYPE_DIRECTORY) {
			obj->prev_dir_id = last_dir_id;
			last_dir_id = obj->id;
		} else
			obj->prev_dir_id = 0;
		if (strcmp(parent->path_name, ".") == 0) {
			strcpy(obj->path_name, oh->name);
		} else {
			strcpy(obj->path_name, parent->path_name);
			strcat(obj->path_name, "/");
			strcat(obj->path_name, oh->name);
		}
		idx = obj->id % HASH_SIZE;
		obj->next = obj_list[idx];
		obj_list[idx] = obj;
	}

	obj->atime = oh->yst_atime;
	obj->mtime = oh->yst_mtime;

	return obj;
}

void set_dirs_utime(void) {
	unsigned id;
	object *obj;

	id = last_dir_id;
	while (id != 0 && (obj = get_object(id)) != NULL) {
		set_utime(obj->path_name, obj->atime, obj->mtime);
		id = obj->prev_dir_id;
	}
}

static void prt_node(char *name, yaffs_ObjectHeader *oh) {
	object *eq_obj;
	struct tm tm;
	time_t mtime;
	mode_t mode;
	char type;
	char fsize[16];
	char perm[10];

	/* get file type, size, mtine and mode */
	eq_obj = NULL;
	strcpy(fsize, "0");
	mtime = oh->yst_mtime;
	mode  = oh->yst_mode;
	switch(oh->type) {
		case YAFFS_OBJECT_TYPE_FILE:		type = '-';
			snprintf(fsize, sizeof(fsize), "%d", oh->fileSize);
			break;
		case YAFFS_OBJECT_TYPE_DIRECTORY:	type = 'd'; break;
		case YAFFS_OBJECT_TYPE_SYMLINK:		type = 'l'; break;
		case YAFFS_OBJECT_TYPE_HARDLINK:	type = 'h';
			eq_obj = get_object(oh->equivalentObjectId);
			mtime = eq_obj != NULL ? eq_obj->mtime : 0;
			mode = STD_PERMS;
			break;
		case YAFFS_OBJECT_TYPE_SPECIAL:
			switch (oh->yst_mode & S_IFMT) {
				case S_IFBLK:		type = 'b';
					snprintf(fsize, sizeof(fsize), "%d,%4d",
					         major(oh->yst_rdev),
					         minor(oh->yst_rdev));
					break;
				case S_IFCHR:		type = 'c';
					snprintf(fsize, sizeof(fsize), "%d,%4d",
					         major(oh->yst_rdev),
					         minor(oh->yst_rdev));
					break;
				case S_IFIFO:		type = 'p'; break;
				case S_IFSOCK:		type = 's'; break;
				default:		type = '?'; break;
			}
			break;
		default:				type = '?'; break;
	}

	/* get file permissions */
	perm[0] = mode & S_IRUSR ? 'r' : '-';
	perm[1] = mode & S_IWUSR ? 'w' : '-';
	perm[2] = mode & S_IXUSR ? 'x' : '-';
	perm[3] = mode & S_IRGRP ? 'r' : '-';
	perm[4] = mode & S_IWGRP ? 'w' : '-';
	perm[5] = mode & S_IXGRP ? 'x' : '-';
	perm[6] = mode & S_IROTH ? 'r' : '-';
	perm[7] = mode & S_IWOTH ? 'w' : '-';
	perm[8] = mode & S_IXOTH ? 'x' : '-';
	if (mode & S_ISUID) perm[2] = perm[2] == '-' ? 'S' : 's';
	if (mode & S_ISGID) perm[5] = perm[5] == '-' ? 'S' : 's';
	if (mode & S_ISVTX) perm[8] = perm[8] == '-' ? 'T' : 't';
	perm[9] = '\0';

	/* print file infos */
	localtime_r(&mtime, &tm);
	printf("%c%s %8s %4d-%02d-%02d %02d:%02d %s",
	       type, perm, fsize,
	       tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
	       tm.tm_hour, tm.tm_min, name);

	/* link destination */
	if (oh->type == YAFFS_OBJECT_TYPE_HARDLINK) {
		if (eq_obj == NULL)
			printf(" -> !!! Invalid !!!");
		else
			printf(" -> /%s", eq_obj->path_name);
	} else if (oh->type == YAFFS_OBJECT_TYPE_SYMLINK) {
		printf(" -> %s", oh->alias);
	}
	printf("\n");
}

int read_chunk(void);

static struct {
	unsigned objectId;
	int      chunk_no;
	unsigned char data[MAX_CHUNK_SIZE + MAX_SPARE_SIZE];
} saved_chunk;

int next_data_chunk(void) {
	yaffs_PackedTags2 *pt;
	unsigned next_objectId;
	unsigned next_chunkId;
	int ret = 0;

	pt = (yaffs_PackedTags2 *)spare_data;
	next_objectId = pt->t.objectId;
	next_chunkId  = pt->t.chunkId + 1;

	if (saved_chunk.objectId == next_objectId &&	/* use saved chunk ? */
	    next_chunkId == 1) {
		memcpy(chunk_data, saved_chunk.data, chunk_size);
		memcpy(spare_data, saved_chunk.data+chunk_size, spare_size);
		saved_chunk.objectId = 0;
		ret = 1;
	} else {
		if (read_chunk() &&		/* valid next chunk ? */
		    pt->t.objectId == next_objectId &&
		    pt->t.chunkId == next_chunkId) {
			ret = 1;
		}
	}
	return ret;
}

void process_chunk(void) {
	yaffs_ObjectHeader oh;
	yaffs_PackedTags2 *pt;
	object *obj, *eq_obj;
	int out_file, remain, s;

	oh = *(yaffs_ObjectHeader *)chunk_data;
	pt = (yaffs_PackedTags2 *)spare_data;

	if (pt->t.sequenceNumber == 0xffffffff)	/* empty object */
		return;

	if (saved_chunk.objectId != 0 &&	/* saved chunk is not part of object */
	    saved_chunk.objectId != pt->t.objectId) {
		prt_err(0, 0, "Warning: Invalid header at chunk #%d, skipping...",
		        saved_chunk.chunk_no);
		if (++warn_count >= MAX_WARN)
			prt_err(1, 0, "Giving up");
		saved_chunk.objectId = 0;
	}

	if (pt->t.chunkId == 1) {		/* save chunk #1 */
		saved_chunk.objectId = pt->t.objectId;
		saved_chunk.chunk_no = chunk_no;
		memcpy(saved_chunk.data, chunk_data, chunk_size);
		memcpy(saved_chunk.data+chunk_size, spare_data, spare_size);
		return;
	} else if (pt->t.chunkId != 0) {	/* not a new object */
		prt_err(0, 0, "Warning: Invalid header at chunk #%d, skipping...",
		        chunk_no);
		if (++warn_count >= MAX_WARN)
			prt_err(1, 0, "Giving up");
		return;
	}

	obj = add_object(&oh, pt);

	/* listing */
	if (opt_verbose)
		prt_node(obj->path_name, &oh);
	else if (opt_list)
		printf("%s\n", obj->path_name);
	if (opt_list) {
		if (oh.type == YAFFS_OBJECT_TYPE_FILE) {
			remain = oh.fileSize;	/* skip over data chunks */
			while(remain > 0) {
				if (!next_data_chunk())
					prt_err(1, 0, "Broken image file");
				remain -= pt->t.byteCount;
			}
		}
		return;
	}

	switch(oh.type) {
		case YAFFS_OBJECT_TYPE_FILE:
			remain = oh.fileSize;
			out_file = creat(obj->path_name, oh.yst_mode & STD_PERMS);
			if (out_file < 0)
				prt_err(1, errno, "Can't create file %s", obj->path_name);
			while(remain > 0) {
				if (!next_data_chunk())
					prt_err(1, 0, "Broken image file");
				s = (remain < pt->t.byteCount) ? remain : pt->t.byteCount;
				if (safe_write(out_file, chunk_data, s) < 0)
					prt_err(1, errno, "Can't write to %s", obj->path_name);
				remain -= s;
			}
			close(out_file);
			safe_lchown(obj->path_name, oh.yst_uid, oh.yst_gid);
			if ((oh.yst_mode & EXTRA_PERMS) != 0 &&
			    chmod(obj->path_name, oh.yst_mode) < 0)
				prt_err(0, errno, "Warning: Can't chmod %s", obj->path_name);
			break;
		case YAFFS_OBJECT_TYPE_SYMLINK:
			if (symlink(oh.alias, obj->path_name) < 0)
				prt_err(1, errno, "Can't create symlink %s", obj->path_name);
			safe_lchown(obj->path_name, oh.yst_uid, oh.yst_gid);
			break;
		case YAFFS_OBJECT_TYPE_DIRECTORY:
			if (pt->t.objectId != YAFFS_OBJECTID_ROOT &&
			    mkdirpath(obj->path_name, oh.yst_mode & STD_PERMS) < 0)
					prt_err(1, errno, "Can't create directory %s", obj->path_name);
			safe_lchown(obj->path_name, oh.yst_uid, oh.yst_gid);
			if ((pt->t.objectId == YAFFS_OBJECTID_ROOT ||
			     (oh.yst_mode & EXTRA_PERMS) != 0) &&
			    chmod(obj->path_name, oh.yst_mode) < 0)
				prt_err(0, errno, "Warning: Can't chmod %s", obj->path_name);
			break;
		case YAFFS_OBJECT_TYPE_HARDLINK:
			eq_obj = get_object(oh.equivalentObjectId);
			if (eq_obj == NULL)
				prt_err(1, 0, "Invalid equivalentObjectId %u in object %u (%s)",
				        oh.equivalentObjectId, pt->t.objectId, oh.name);
			if (link(eq_obj->path_name, obj->path_name) < 0)
				prt_err(1, errno, "Can't create hardlink %s", obj->path_name);
			break;
		case YAFFS_OBJECT_TYPE_SPECIAL:
			if (mknod(obj->path_name, oh.yst_mode, oh.yst_rdev) < 0) {
				if (errno == EPERM || errno == EINVAL)
					prt_err(0, errno, "Warning: Can't create device %s", obj->path_name);
				else
					prt_err(1, errno, "Can't create device %s", obj->path_name);
			} else
				safe_lchown(obj->path_name, oh.yst_uid, oh.yst_gid);
			break;
		case YAFFS_OBJECT_TYPE_UNKNOWN:
			break;
	}

	/* set file date and time */
	switch(oh.type) {
		case YAFFS_OBJECT_TYPE_FILE:
		case YAFFS_OBJECT_TYPE_SPECIAL:
#ifdef HAS_LUTIMES
		case YAFFS_OBJECT_TYPE_SYMLINK:
#endif
			set_utime(obj->path_name,
			          oh.yst_atime, oh.yst_mtime);
			break;
		case YAFFS_OBJECT_TYPE_DIRECTORY:
		default:
			break;
	}
}


int read_chunk(void) {
	ssize_t s, len, offset;

	chunk_no++;
	len = chunk_size + spare_size;
	offset = 0;
	memset(data, 0xff, len);

	if (buf_len > buf_idx) {		/* copy from buffer */
		s = buf_len - buf_idx;
		if (s > len) s = len;
		memcpy(data, buffer+buf_idx, s);
		buf_idx += s; offset += s;
	}

	if (offset < len) {			/* read from file */
		s = safe_read(img_file, data+offset, len-offset);
		if (s < 0)
			prt_err(1, errno, "Read image file");
		offset += s;
	}

	if (offset != 0 && offset != len)	/* partial chunk */
		prt_err(1, 0, "Broken image file");

	if (offset == len && spare_off != 0) {	/* bad block info */
		memmove(data+chunk_size, data+chunk_size+spare_off,
		        spare_size-spare_off);
		memset(data+len-spare_off, 0xff, spare_off);
	}

	return offset != 0;
}

int check_layout(int chunk, int spare, int off) {
	yaffs_ObjectHeader oh;
	yaffs_PackedTags2  pt;
	int i, detect;

	for (i = 0, detect = 1; i < 4 && detect; i++) {
		memcpy(&oh, buffer + i * (chunk+spare), sizeof(oh));
		memcpy(&pt, buffer + i * (chunk+spare) + chunk+off, sizeof(pt));

		detect =
		  (pt.t.chunkId > 0 && pt.t.chunkId <= 10 &&
		   pt.t.objectId >= 0x100 && pt.t.objectId <= (0x100+10) &&
		   pt.t.byteCount <= chunk) ||
		  (pt.t.chunkId == 0 &&
		   (pt.t.objectId == YAFFS_OBJECTID_ROOT ||
		    (pt.t.objectId >= 0x100 && pt.t.objectId <= (0x100+10))) &&
		   pt.t.byteCount == 0xffff &&
		   (oh.type == YAFFS_OBJECT_TYPE_FILE ||
		    oh.type == YAFFS_OBJECT_TYPE_DIRECTORY ||
		    oh.type == YAFFS_OBJECT_TYPE_SYMLINK ||
		    oh.type == YAFFS_OBJECT_TYPE_HARDLINK ||
		    oh.type == YAFFS_OBJECT_TYPE_SPECIAL));
	}

	return detect;
}

void detect_flash_layout(int show, int first) {
	int cnt;
	int chunk, spare, off;

	memset(buffer, 0xff, sizeof(buffer));
	buf_len = safe_read(img_file, buffer, sizeof(buffer));
	if (buf_len < 0)
		prt_err(1, errno, "Read image file");

	if (show)
		printf("Detected flash layout(s):\n");

	cnt = 0;
	for (chunk = MIN_CHUNK_SIZE; chunk <= MAX_CHUNK_SIZE; chunk *= 2) {
		for (spare = MIN_SPARE_SIZE; spare <= MAX_SPARE_SIZE; spare += 16) {
			for (off = 0; off <= 2; off += 2) {
				if (check_layout(chunk, spare, off)) {
					cnt++;
					if (show) {
						printf("%2s -c %-2d -s %-3d : chunk size = %2dK, spare size = %3d, %sbad block info\n",
						       off ? "-b" : "", chunk / 1024, spare,
						       chunk / 1024, spare, off ? "" : "no ");
					}
					if (first) {
						chunk_size = chunk;
						spare_size = spare;
						spare_off  = off;
						return;
					}
				}
			}
		}
	}

	if (cnt == 0) {
		if (show) {
			printf("-- none --\n");
			exit(1);
		} else {
			prt_err(1, 0, "Can't determine flash layout, perhaps not a yaffs2 image");
		}
	}
}

void usage(void) {
	fprintf(stderr, "\
unyaffs V%s - extract files from a YAFFS2 file system image.\n\
\n\
Usage: unyaffs [options] <image_file_name> [<extract_directory>]\n\
\n\
Options:\n\
    -d               detection of flash layout, no extraction\n\
    -b               spare contains bad block information\n\
    -c <chunk size>  set chunk size in KByte (default: autodetect, max: %d)\n\
    -s <spare size>  set spare size in Byte  (default: autodetect, max: %d)\n\
    -t               list image contents\n\
    -v               verbose output\n\
    -V               print version\n\
", VERSION, MAX_CHUNK_SIZE / 1024, MAX_SPARE_SIZE);
	exit(1);
}

int main(int argc, char **argv) {
	int ch;
	char *ep;

	int opt_detect;
	int opt_bad;
	int opt_chunk;
	int opt_spare;

	/* handle command line options */
	opt_detect = 0;
	opt_bad = 0;
	opt_chunk = 0;
	opt_spare = 0;
	opt_list = 0;
	opt_verbose = 0;
	while ((ch = getopt(argc, argv, "dbc:s:tvVh?")) > 0) {
		switch (ch) {
			case 'd':
				opt_detect = 1;
				break;
			case 'b':
				opt_bad = 1;
				break;
			case 'c':
				opt_chunk = strtol(optarg, &ep, 0);
				if (*ep != '\0' ||
				    opt_chunk < 0 ||
				    opt_chunk > (MAX_CHUNK_SIZE / 1024) )
					usage();
				break;
			case 's':
				opt_spare = strtol(optarg, &ep, 0);
				if (*ep != '\0' ||
				    opt_spare < 0 ||
				    opt_spare > MAX_SPARE_SIZE)
					usage();
				break;
			case 't':
				opt_list = 1;
				break;
			case 'v':
				opt_verbose = 1;
				break;
			case 'V':
				printf("V%s\n", VERSION);
				exit(0);
				break;
			case 'h':
			case '?':
			default:
				usage();
				break;
    		}
	}

	/* extract rest of command line parameters */
	if ((argc - optind) < 1 || (argc - optind) > 2)
		usage();

	if (strcmp(argv[optind], "-") == 0) {	/* image file from stdin ? */
		img_file = 0;
	} else {
		img_file = open(argv[optind], O_RDONLY);
		if (img_file < 0)
			prt_err(1, errno, "Open image file failed");
	}

	if (opt_detect) {
		detect_flash_layout(1, 0);
		return 0;
	}

	if (opt_chunk == 0 || opt_spare == 0) {
		detect_flash_layout(0, 1);
		if (opt_verbose)
			prt_err(0, 0,
		        	"Header check OK, chunk size = %dK, spare size = %d, %sbad block info.",
		        	chunk_size/1024, spare_size, spare_off ? "" : "no ");
	} else {
		chunk_size = opt_chunk * 1024;
		spare_size = opt_spare;
		spare_off  = opt_bad ? 2 : 0;
	}
	spare_data = data + chunk_size;

	if ((argc - optind) == 2 && !opt_list) {
		if (mkdirpath(argv[optind+1], 0755) < 0)
			prt_err(1, errno, "Can't mkdir %s", argv[optind+1]);
		if (chdir(argv[optind+1]) < 0)
			prt_err(1, errno, "Can't chdir to %s", argv[optind+1]);
	}

	umask(0);

	init_obj_list();
	saved_chunk.objectId = 0;
	while (read_chunk()) {
		process_chunk();
	}
	set_dirs_utime();
	close(img_file);

	if (warn_chown)
#ifdef __CYGWIN__
		prt_err(0, 0, "Warning: Can't restore owner/group attribute (limitation of Cygwin/Windows)");
#else
		prt_err(0, 0, "Warning: Can't restore owner/group attribute, run unyaffs as root");
#endif

	return 0;
}
