/*
 *  exifrescue -- salvage EXIF and/or JFIF files from disk image
 *	ディスクイメージファイル、あるいはローデバイスを読んで
 *	JPEGファイルをサルベージする
 *
 *  ※	wipe-out で全消去したデバイスやイメージからは
 *  ※	逆立ちしてもJPEGファイルをサルベージできません
 *
 *  revision history
 *	0.0: Sep. 22, 2011 by Dai ISHIJIMA
 *	0.1: Aug.  9, 2013 (offset, strtol)
 *
 *  example:
 *	% exifrescue disk.img
 *	% exifrescue -v -l 4m -m 8g /dev/da0s1
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>

#ifdef NO_OFF_T_FSEEKO
#define off_t long
#define fseeko fseek
#define ftello ftell
#endif

#define YES 1
#define NO  0

char *prog;
#define shift --argc; ++argv

#define MAXBUF 65536
#define MAXPATH 4096

#define BLKSIZ (1 * 1024)
#define MINLEN (64 * 1024)
#define MAXLEN (2 * 1024 * 1024)
#define OFSLEN 0
#define IMGSIZ ((long long int)8 * 1024 * 1024 * 1024)

#define IS_EXIF 'e'
#define IS_JFIF 'j'

#define SOI 0xffd8
#define FORMOFS 6
#define FORMLEN 4
#define EXIF "Exif"
#define JFIF "JFIF"


#define hi(x) (((x) >> 8) & 0xff)
#define lo(x) ((x) & 0xff)
#define hilo(x,y) (((x) << 8) + (y))


/* JPEGの先頭マーカー (SOI) か? */
int is_soi(unsigned char *buf)
{
    if (hilo(buf[0], buf[1]) == SOI) {
	return(YES);
    }
    return(NO);
}


/* JPEGファイルの先頭か */
int isheader(unsigned char *buf, int len, int chkeof)
{
    int n;

    if (buf == NULL) {
	return(-1);
    }
    if (is_soi(buf)) {
	if (strncasecmp(EXIF, (char *)(&buf[FORMOFS]), FORMLEN) == 0) {
	    return(IS_EXIF);
	}
	if (strncasecmp(JFIF, (char *)(&buf[FORMOFS]), FORMLEN) == 0) {
	    return(IS_JFIF);
	}
    }
    if (chkeof) {	/* 00 か ff が連続していたらJPEGの終了と判断 */
	for (n = 1; n < len; n++) {
	    if (buf[n] != buf[0]) {
		return(0);
	    }
	}
	if ((buf[0] == 0) || (buf[0] == 0xff)) {
	    return(-1);
	}
    }
    return(0);
}


/* イメージファイルを先頭からスキャンしてJPEG画像を復元 */
void salvage(FILE *fp, int blksiz, int minlen, int maxlen, int ofslen,
	     off_t imgsiz, int chkeof, char *path, int verbose)
{
    off_t pos;
    off_t nextpos, startpos;
    int len;
    FILE *ofp;
    unsigned char buf[MAXBUF];
    int type;
    char filnam[MAXPATH];
    int n;

    n = 0;
    if (imgsiz <= 0) {
	if (fseeko(fp, 0, SEEK_END) < 0) {
	    fprintf(stderr, "%s: not a seekable stream\n", prog);
	    exit(1);
	}
	imgsiz = ftello(fp);
    }
    if (verbose > 0) {
	fprintf(stderr, "%s: imgsiz: %lld\n", prog, (long long int)imgsiz);
    }
    if (imgsiz <= 0) {
	fprintf(stderr, "%s: imgsiz: %lld, assumed %lld\n",
		prog, (long long int)imgsiz, IMGSIZ);
	imgsiz = IMGSIZ;
    }
    fseeko(fp, ofslen, SEEK_SET);
    nextpos = startpos = pos = ftello(fp);
    if (pos < 0) {	/* fseek/fseekoできないときは負のはず */
	fprintf(stderr, "%s: not a seekable stream\n", prog);
	exit(1);
    }
    if (verbose > 0) {
	fprintf(stderr, "%s: start at %08lx\n", prog, (unsigned long)pos);
    }
    len = fread(buf, 1, blksiz, fp);
    ofp = NULL;
    while (len > 0) {
	type = isheader(buf, len, chkeof);
	if ((type == IS_EXIF) || (type == IS_JFIF)) {	/* JPEGファイル発見 */
	    if (verbose > 0) {
		fprintf(stderr, "%s: JPEG found, type %c, at %08lx (%d%%)\n",
			prog, type, (unsigned long)pos,
			(unsigned int)(100 * pos / imgsiz));
	    }
	    if (ofp == NULL) {				/* ファイルに出力 */
		nextpos = startpos = pos;
		sprintf(filnam, "%s%c%07lx.jpg", path, type,
			(unsigned long)(pos / BLKSIZ));
		if (verbose > 0) {
		    fprintf(stderr, "%s: open %s\n", prog, filnam);
		}
		if ((ofp = fopen(filnam, "w")) == NULL) {
		    fprintf(stderr, "%s: can't open %s\n", prog, filnam);
		    exit(1);
		}
	    }
	    else {	/* ファイル出力中なら次の開始位置を記憶 */
		if (pos - startpos < minlen) {
		    if (nextpos <= startpos) {
			nextpos = pos;
			if (verbose > 1) {
			    fprintf(stderr, "%s: set next pos: %08lx\n",
				    prog, (unsigned long)nextpos);
			}
		    }
		}
		else {	/* ある程度の長さを出力していたら閉じる */
		    if (verbose > 1) {
			fprintf(stderr, "%s: file closed at %08lx\n",
				prog, (unsigned long)pos);
		    }
		    fclose(ofp);
		    ++n;
		    ofp = NULL;
		    if (nextpos <= startpos) {
			nextpos = pos;
		    }
		    fseeko(fp, nextpos, SEEK_SET);
		}
	    }
	}
	if (ofp != NULL) {	/* ファイル出力中 */
	    if (type >= 0) {
		fwrite(buf, 1, len, ofp);
	    }
	    if ((pos - startpos > maxlen) || (type < 0)) {
		/* ファイルの終端に達したと判断したら閉じる */
		if (verbose > 1) {
		    if (type < 0) {
			fprintf(stderr, "%s: detect EOF\n", prog);
		    }
		    fprintf(stderr, "%s: file closed at %08lx\n",
			    prog, (unsigned long)pos);
		}
		fclose(ofp);
		++n;
		ofp = NULL;
		if (nextpos > startpos) {
		    fseeko(fp, nextpos, SEEK_SET);
		}
	    }
	}
	if ((pos = ftello(fp)) < 0) {
	    fprintf(stderr, "%s: not a seekable stream\n", prog);
	    exit(1);
	}
	len = fread(buf, 1, blksiz, fp);
    }
    if (ofp != NULL) {
	fclose(ofp);
	++n;
    }
    if (verbose > 1) {
	fprintf(stderr, "%s: wrote %d file(s)\n", prog, n);
    }
}
		

void usage()
{
    fprintf(stderr, "Usage: %s [option] [disk image]\n", prog);
    fprintf(stderr, "    options:\n");
    fprintf(stderr, "\t-b # set block size [%d]\n", BLKSIZ);
    fprintf(stderr, "\t-s # set minimum length [%d]\n", MINLEN);
    fprintf(stderr, "\t-l # set maximum length [%d]\n", MAXLEN);
    fprintf(stderr, "\t-m # set disk image length (auto detect)\n");
    fprintf(stderr, "\t-o # set offset length [%d]\n", OFSLEN);
    fprintf(stderr, "\t-x # same as -o, but uses strtol()\n");
    fprintf(stderr, "\t-e detect end of file\n");
    fprintf(stderr, "\t-p path set output path [current directory]\n");
    fprintf(stderr, "\t-v be verbose\n");
}


/* k や m がついていたら、kバイト、Mバイトと解釈 */
long long int bytesize(char *s)
{
    long long int n;

    n = atoi(s);
    if (s[strlen(s) - 1] == 'k') {
	n *= 1024;
    }
    if (s[strlen(s) - 1] == 'm') {
	n *= (1024 * 1024);
    }
    if (s[strlen(s) - 1] == 'g') {
	n *= (1024 * 1024 * 1024);
    }
    return(n);
}


int main(int argc, char *argv[])
{
    int blksiz, minlen, maxlen, ofslen;
    char path[MAXPATH];
    FILE *fp;
    int chkeof;
    int verbose;
    off_t imgsiz;

    prog = *argv;
    shift;
    blksiz = minlen = maxlen = 0;
    ofslen = -1;
    verbose = 0;
    chkeof = NO;
    strcpy(path, "");
    while ((argc > 0) && (argv[0][0] == '-')) {
	if (argv[0][1] == 'b') {
	    shift;
	    blksiz = bytesize(*argv);
	}
	else if (argv[0][1] == 's') {
	    shift;
	    minlen = bytesize(*argv);
	}
	else if (argv[0][1] == 'l') {
	    shift;
	    maxlen = bytesize(*argv);
	}
	else if (argv[0][1] == 'm') {
	    shift;
	    imgsiz = bytesize(*argv);
	}
	else if (argv[0][1] == 'o') {
	    shift;
	    ofslen = bytesize(*argv);
	}
	else if (argv[0][1] == 'x') {
	    shift;
	    ofslen = strtol(*argv, (char **)NULL, 0);
	}
	else if (argv[0][1] == 'y') {
	    shift;
	    ofslen = strtol(*argv, (char **)NULL, 0) - FORMOFS;
	}
	else if (argv[0][1] == 'e') {
	    chkeof = !chkeof;
	}
	else if (argv[0][1] == 'p') {
	    shift;
	    strcpy(path, *argv);
	}
	else if (argv[0][1] == 'v') {
	    ++verbose;
	}
	else {
	    usage();
	    exit(1);
	}
	shift;
    }
    if (blksiz <= 0) {
	blksiz = BLKSIZ;
    }
    if (minlen <= 0) {
	minlen = MINLEN;
    }
    if (maxlen <= 0) {
	maxlen = MAXLEN;
    }
    if (ofslen < 0) {
	ofslen = OFSLEN;
    }
    if ((strlen(path) > 0) && (path[strlen(path) - 1] != '/')) {
	strcat(path, "/");
    }
    if (argc > 0) {
	while (argc > 0) {
	    if ((fp = fopen(*argv, "r")) == NULL) {
		fprintf(stderr, "%s: can't open %s\n", prog, *argv);
		exit(1);
	    }
	    if (verbose > 0) {
		fprintf(stderr,
			"%s: file %s, -b=%d -s=%d -l=%d -o=%d -e=%d -p=%s\n",
			prog, *argv, blksiz, minlen, maxlen, ofslen,
			chkeof, path);
	    }
	    salvage(fp, blksiz, minlen, maxlen, ofslen, 
		    imgsiz, chkeof, path, verbose);
	    shift;
	}
    }
    else {
	salvage(stdin, blksiz, minlen, maxlen, ofslen,
		imgsiz, chkeof, path, verbose);
    }
    exit(0);
}


/* Local Variables: */
/* compile-command:"cc -Wall -o exifrescue exifrescue.c" */
/* End: */
