
static char rcsid[] = "$Id: isoextract.c,v 1.10 2003/02/06 21:36:32 eric Exp $";

/*
 * isoextract - extract files from iso9660 filesystem.
 *
 
 Written by Eric Lammerts <eric@scintilla.utwente.nl> (1999).
 
 Based on isotool, written by Eric Youngdale (1993).
 
 Copyright 1993 Yggdrasil Computing, Incorporated

 
 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, or (at your option)
 any later version.
 
 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.
 
 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.  */


#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <linux/cdrom.h>
#include <linux/iso_fs.h>

#include "md5.h"

#define CHECK fprintf(stderr,"line %d\n",__LINE__);

int use_rock = 1;
int use_joliet = 1;
int debug = 0;
int verbose = 0;
int ivd_offset = -1;
int block_size = 2048;
int block_offset = 0;

char *isodev;
int isofd;
off_t isooff = 0;
struct iso_primary_descriptor ivd;
int joliet = 0;
char volid[33] = "";

struct todo {
	struct todo *next;
	char *name;
	int extent;
	int length;
} *dtodo = NULL, *ftodo = NULL;

void isoread(char *buf, int size)
{
	int r;

	if(debug > 1) printf("reading %d bytes at offset %d\n", size, (int)isooff);
	while(size) {
		if((r = read(isofd, buf, size)) == -1) {
			if(errno == EINTR) continue;
			perror(isodev);
			exit(1);
		} else if(r == 0) {
			fprintf(stderr, "Premature end-of-file in %s.\n", isodev);
			exit(1);
		}
		size -= r;
		buf += r;
		isooff += r;
	}
}

void isoseek(int extent)
{
	off_t newisooff;

	newisooff = extent * block_size + block_offset;

	if(debug > 1) printf("%sseeking to %d*%d+%d = %d\n",
		newisooff == isooff? "not ":"", extent, block_size, block_offset, (int)newisooff);

	if(newisooff == isooff) return;
	
	if(lseek(isofd, newisooff, SEEK_SET) >= 0) {
		isooff = newisooff;
	} else {
		if(errno == ESPIPE) {
			int r;
			char buf[2048];

			if(newisooff < isooff) {
				fprintf(stderr, "%s: can't seek back.\n", isodev);
				exit(1);
			}
			while(isooff < newisooff) {
				r = newisooff - isooff;
				if(r > 2048) r = 2048;
				isoread(buf, r);
			}
		} else {
			fprintf(stderr, "%s: can't seek to position %d (file truncated?).\n",
				isodev, (int)newisooff);
			exit(1);
		}
	}
}

int isonum_731(char *p)
{
	return ((p[0] & 0xff)
			| ((p[1] & 0xff) << 8)
			| ((p[2] & 0xff) << 16)
			| ((p[3] & 0xff) << 24));
}

int isonum_733(unsigned char *p)
{
	return (isonum_731(p));
}

void unicode2ascii(char *dst, int dlen, char *src, int slen)
{
	int i;

	for(i = 0; i < dlen && (i * 2) < slen; i++)
		dst[i] = src[i * 2]? '?' : src[i * 2 + 1];
	for(; i < dlen; i++)
		dst[i] = ' ';
}

int read_voldesc(struct iso_primary_descriptor *ivd, int joliet)
{
	struct iso_primary_descriptor ipvd;
	int ipvd_found = 0;
	struct cdrom_multisession msinfo;

	if(ivd_offset == -1) {
		msinfo.addr_format = CDROM_LBA;
		if (ioctl(isofd, CDROMMULTISESSION, &msinfo) != -1 && msinfo.xa_flag)
			ivd_offset = msinfo.addr.lba + 16;
		else
			ivd_offset = 16;
		if(debug)
			fprintf(stderr,"Last session starts at extent %d\n", ivd_offset);
	}

	for(;; ivd_offset++) {
		isoseek(ivd_offset);
		isoread((char *)&ipvd, sizeof(ipvd));

		if(memcmp(ipvd.id, "CD001", 5) != 0) {
			if(ivd_offset == 16 && block_size == 2048 && block_offset == 0) {
				/* try 2352-byte sectors */
				block_size = 2352;
				block_offset = 16;
				return read_voldesc(ivd, joliet);
			}
			if(ivd_offset == 16 && block_size == 2352 && block_offset == 16) {
				/* try offset 24 */
				block_offset = 24;
				return read_voldesc(ivd, joliet);
			}
			fprintf(stderr, "Invalid volume descriptor at sector %d\n", ivd_offset);
			exit(1);
		}
		if ((unsigned char)ipvd.type[0] == ISO_VD_END)
			break;

		if ((unsigned char)ipvd.type[0] == ISO_VD_PRIMARY) {
			memcpy(ivd, &ipvd, sizeof(struct iso_primary_descriptor));
			ipvd_found = 1;
			if(debug)
				fprintf(stderr,"Pri.desc. found at extent %d\n", ivd_offset);
		}
		if (joliet && ipvd.type[0] == ISO_VD_SUPPLEMENTARY) {
			struct iso_supplementary_descriptor *isvd =
				(struct iso_supplementary_descriptor *)&ipvd;

			if(isvd->escape[0] == '%' && isvd->escape[1] == '/'
			   && (isvd->escape[2] == '@' ||
			       isvd->escape[2] == 'C' ||
				   isvd->escape[2] == 'E')) {
				if(debug)
					fprintf(stderr,"Joliet.desc. found at extent %d\n", ivd_offset);
				unicode2ascii(isvd->system_id, 32, isvd->system_id, 32);
				unicode2ascii(isvd->volume_id, 32, isvd->volume_id, 32);
				memcpy(ivd, isvd, sizeof(struct iso_primary_descriptor));
				return 0;
			}
		}
	}
	if(!ipvd_found) return -1;
	return 0;
}
/* add entry to list, sorted by extent */
void add_todo(struct todo **list, char *name, int extent, int size)
{
	struct todo **p, *new;

	for(p = list; *p && (*p)->extent < extent; p = &(*p)->next) ;
	new = (struct todo *)malloc(sizeof(struct todo));
	new->next = *p;
	new->name = strdup(name);
	new->extent = extent;
	new->length = size;
	*p = new;
}

void parse_rr(unsigned char *pnt, int len, char *filename)
{
	int cont_extent = 0, cont_offset = 0, cont_size = 0;

	while (len >= 4) {
		if (pnt[3] != 1) {
			fprintf(stderr,"**BAD RRVERSION\n");
			return;
		};
		if (strncmp(pnt, "NM", 2) == 0) {
			strncpy(filename, pnt + 5, pnt[2] - 5);
			filename[pnt[2] - 5] = 0;
		}
		if (strncmp(pnt, "CE", 2) == 0) {
			cont_extent = isonum_733(pnt + 4);
			cont_offset = isonum_733(pnt + 12);
			cont_size = isonum_733(pnt + 20);
		};

		len -= pnt[2];
		pnt += pnt[2];
		if (len <= 3 && cont_extent) {
			unsigned char sector[ISOFS_BLOCK_SIZE];

			isoseek(cont_extent);
			isoread(sector, sizeof(sector));
			parse_rr(&sector[cont_offset], cont_size, filename);
		};
	};
}

void dump_rr(struct iso_directory_record *idr, char *filename)
{
	int off;

	off = sizeof(*idr) - sizeof(idr->name) + idr->name_len[0] +
	      ((idr->name_len[0] & 1)? 0 : 1);
	parse_rr((unsigned char *)idr + off, (unsigned char)idr->length[0] - off, filename);
}

void parse_dir(struct todo *dir)
{
	char fullname[256], filename[256], *p;
	struct iso_directory_record *idr;
	int i;
	char sector[ISOFS_BLOCK_SIZE];

	while (dir->length > 0) {
		isoseek(dir->extent);
		isoread(sector, sizeof(sector));
		dir->length -= sizeof(sector);
		dir->extent++;

		for(i = 0; i < ISOFS_BLOCK_SIZE; i += (unsigned char)idr->length[0]) {
			idr = (struct iso_directory_record *)(sector + i);
			if(idr->length[0] == 0) break;

			if(idr->flags[0] & 5) continue;
			if (idr->name_len[0] == 1 && idr->name[0] <= 1) continue;
			filename[0] = 0;

			if(joliet) {
				unicode2ascii(filename, (unsigned char)idr->name_len[0] / 2,
				              idr->name, (unsigned char)idr->name_len[0]);
				filename[idr->name_len[0] / 2] = 0;
			} else {
				strncpy(filename, idr->name, idr->name_len[0]);
				filename[idr->name_len[0]] = 0;
			}
			if((p = strchr(filename, ';')))
				*p = 0;

			if(use_rock)
				dump_rr(idr, filename);

			snprintf(fullname, sizeof(fullname), "%s/%s", dir->name, filename);

			if ((idr->flags[0] & 2) != 0) {
				/* directory */
				if(debug)
					fprintf(stderr,"Adding dir  %s to list\n", fullname);
				add_todo(&dtodo, fullname, isonum_733(idr->extent), isonum_733(idr->size));
			} else {
				/* file */
				if(debug > 1)
					fprintf(stderr,"Adding file %s to list\n", fullname);
				add_todo(&ftodo, fullname, isonum_733(idr->extent), isonum_733(idr->size));
			}
		}
	}
}

void extract_file(struct todo *file)
{
	int fd, len, w, pos, extent;
	char buf[2048];

	if((fd = creat(file->name + 1, 0666)) == -1) {
		perror(file->name + 1);
		exit(1);
	}
	extent = file->extent;
	while(file->length) {
		len = file->length < sizeof(buf)? file->length : sizeof(buf);
		isoseek(extent);
		isoread(buf, 2048);
		file->length -= len;
		pos = 0;
		while(len) {
			if((w = write(fd, buf + pos, len)) == -1) {
				if(errno == EINTR) continue;
				perror(file->name);
				exit(1);
			}
			len -= w;
			pos += w;
		}
		extent++;
	}
	if(close(fd) == -1) {
		perror(file->name);
		exit(1);
	}
}

void md5_file(struct todo *file)
{
	int len, extent;
	unsigned char buf[2048];
	MD5_CTX md5;

	MD5Init(&md5);
	extent = file->extent;
	
	while(file->length) {
		len = file->length < sizeof(buf)? file->length : sizeof(buf);
		isoseek(extent);
		isoread(buf, 2048);
		file->length -= len;
		MD5Update(&md5, buf, len);
		extent++;
	}
	MD5Final(buf, &md5);
	printf("%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x  %s\n",
		buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7],
		buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], buf[15], file->name + 1);
}

void usage(FILE *f)
{
	fprintf(f,
		"Usage:   isoextract [-dehmnvRJV] [-D device] [-s extent] [-o outputdir]\n"
		"                    [-O offset] [-S size] [device|file]\n"
		"\n"
		"         -d           : increase debugging level\n"
		"         -e           : eject cdrom afterwards\n"
		"         -h           : show usage\n"
		"         -m           : print md5sum of each file instead of extracting it\n"
		"         -n           : only print file names, don't extract or md5sum\n"
		"         -o outputdir : put files in outputdir\n"
		"         -s extent    : look for volume descriptors starting at extent\n"
		"         -v           : print names of extracted files\n"
		"         -R           : don't use Rock Ridge extensions\n"
		"         -J           : don't use Joliet extensions\n"
		"         -D device    : use this instead of /dev/cdrom\n"
		"         -O offset    : offset within block\n"
		"         -S size      : block size\n"
		"         -V           : append volume ID to outputdir\n"
		"\n");
	exit(1);
}

int main(int argc, char *argv[])
{
	int i, c, add_volid = 0, md5 = 0, eject = 0, print_only = 0;
	struct iso_directory_record *idr;
	struct todo *this;
	char *outputdir = NULL;

	while ((c = getopt(argc, argv, "dehmno:s:vRJD:O:S:V")) != EOF) switch (c) {
		case 'd':
			debug++;
			break;
		case 'e':
			eject++;
			break;
		case 'h':
			usage(stdout);
		case 'm':
			md5 = 1;
			setlinebuf(stdout);
			break;
		case 'n':
			print_only++;
			break;
		case 'o':
			outputdir = optarg;
			break;
		case 's':
			ivd_offset = atoi(optarg);
			break;
		case 'v':
			verbose++;
			break;
		case 'R':
			use_rock = 0;
			break;
		case 'J':
			use_joliet = 0;
			break;
		case 'D':
			isodev = optarg;
			break;
		case 'O':
			block_offset = atoi(optarg);
			break;
		case 'S':
			block_size = atoi(optarg);
			break;
		case 'V':
			add_volid = 1;
			break;
		default:
			usage(stderr);
	}

	if(isodev == NULL) {
		if(optind < argc) {
			isodev = argv[optind++];
		} else {
			isodev = "/dev/cdrom";
		}
	}
	if(strcmp(isodev, "-") == 0) {
		isofd = 0;
	} else if((isofd = open(isodev, O_RDONLY)) == -1) {
		perror(isodev);
		exit(1);
	}
	if(read_voldesc(&ivd, use_joliet) == -1) {
		perror(isodev);
		exit(1);
	}
	if(ivd.type[0] == ISO_VD_SUPPLEMENTARY) {
		joliet = 1;
		use_rock = 0;
		if(debug)
			fprintf(stderr,"Joliet CD detected, disabling Rock Ridge extensions.\n");
	}

	if(!md5 && outputdir && chdir(outputdir) == -1) {
		perror(outputdir);
		exit(1);
	}

	if(!md5 && add_volid) {
		memcpy(volid, ivd.volume_id, 32);
		volid[32] = 0;

		for(i = 31; i >= 0 && volid[i] == ' '; i--) volid[i] = 0;

		if(volid[0] == '.') volid[0] = '_';
		for(i = 0; volid[i]; i++) if(volid[i] == '/') volid[i] = '_';

		strcat(volid, "/");

		if(mkdir(volid, 0777) == -1 && errno != EEXIST) {
			perror(volid);
			exit(1);
		}
		if(chdir(volid) == -1) {
			perror(volid);
			exit(1);
		}
	}

	idr = (struct iso_directory_record *)&ivd.root_directory_record;
	if(debug) {
		printf(" idr->length=%d\n", idr->length[0]);
		printf(" idr->ext_attr_length=%d\n", idr->length[0]);
		printf(" idr->extent=%d\n", isonum_733(idr->extent));
		printf(" idr->size=%d\n", isonum_733(idr->size));
		printf(" idr->flags=0x%02x\n", idr->flags[0]);
	}
	add_todo(&dtodo, "",  isonum_733(idr->extent), isonum_733(idr->size));

	while(dtodo) {
		this = dtodo;
		dtodo = this->next;

		if(debug || (verbose && this->name[0])) {
			printf("%s%s/", volid, this->name[0]? this->name + 1 : "");
			if(debug)
				printf(" d (e=%d l=%d)", this->extent, this->length);
			printf("\n");
		}
		if(!md5 && !print_only && this->name[0] && mkdir(this->name + 1, 0777) == -1 && errno != EEXIST) {
			fprintf(stderr, "Can't make directory %s: %s\n",
				this->name + 1, strerror(errno));
			exit(1);
		}
		parse_dir(this);
		free(this->name);
		free(this);
	}
	
	while(ftodo) {
		this = ftodo;
		ftodo = this->next;

		if(debug || verbose) {
			printf("%s%s", volid, this->name + 1);
			if(debug)
				printf(" (f e=%d l=%d)", this->extent, this->length);
			printf("\n");
		}

		if(print_only == 1) {
			printf("%s\n", this->name);
		} else if(print_only == 2) {
			printf("%9d %s\n", this->length, this->name);
		} else if(md5) {
			md5_file(this);
		} else {
			extract_file(this);
		}

		free(this->name);
		free(this);
	}

	if(eject && ioctl(isofd, CDROMEJECT, 0) == -1 && errno != ENOTTY)
		perror("eject");

	close(isofd);
	return 0;
}
