/* reloc65 -- A part of xa65 - 65xx/65816 cross-assembler and utility suite
 * o65 file relocator
 *
 * Copyright (C) 1989-1997 André Fachat (a.fachat@physik.tu-chemnitz.de)
 *
 * 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.
 *
 * 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 <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

#include "version.h"

#define	BUF	(9*2+8)		/* 16 bit header */

#define programname	"reloc65"
#define progversion	"v0.2.1"
#define author		"Written by Andre Fachat"
#define copyright	"Copyright (C) 1997-2002 Andre Fachat."

typedef struct {
	char 		*fname;
	size_t 		fsize;
	unsigned char	*buf;
	int		tbase, tlen, dbase, dlen, bbase, blen, zbase, zlen;
	int		tdiff, ddiff, bdiff, zdiff;
	unsigned char	*segt;
	unsigned char	*segd;
	unsigned char 	*utab;
	unsigned char 	*rttab;
	unsigned char 	*rdtab;
	unsigned char 	*extab;
} file65;


int read_options(unsigned char *f);
int read_undef(unsigned char *f);
unsigned char *reloc_seg(unsigned char *f, int len, unsigned char *rtab, file65 *fp, int undefwarn);
unsigned char *reloc_globals(unsigned char *, file65 *fp);

file65 file;
unsigned char cmp[] = { 1, 0, 'o', '6', '5' };

void usage(FILE *fp)
{
	fprintf(fp,
		"Usage: %s [OPTION]... [FILE]...\n"
		"Relocator for o65 object files\n"
		"\n"
		"  -b? addr   relocates segment '?' (i.e. 't' for text segment,\n"
		"               'd' for data, 'b' for bss and 'z' for zeropage) to the new\n"
		"               address `addr'\n"
		"  -o file    uses `file' as output file. Default is `a.o65'\n"
		"  -x?        extracts text `?' = `t' or data `?' = `d' segment from file\n",
		programname);
	fprintf(fp,
		"               instead of writing back the whole file\n"
		"  -X         extracts the file such that text and data\n"
		"               segments are chained, i.e. possibly relocating\n"
		"               the data segment to the end of the text segment\n" 
		"  -v         verbose output\n"
		"  --version  output version information and exit\n"
		"  --help     display this help and exit\n");
}

int main(int argc, char *argv[]) {
	int i = 1, mode, hlen;
	size_t n;
	FILE *fp;
	int tflag = 0, dflag = 0, bflag = 0, zflag = 0;
	int tbase = 0, dbase = 0, bbase = 0, zbase = 0;
	int *base;
	char *outfile = "a.o65";
	int extract = 0;
	int verbose = 0;

	if (argc <= 1) {
	  usage(stderr);
	  exit(1);
	}

	if (strstr(argv[1], "--help") || strstr(argv[1], "-?")) {
          usage(stdout);
	  exit(0);
	}

	if (strstr(argv[1], "--version")) {
          version(programname, progversion, author, copyright);
	  exit(0);
	}

	while(i<argc) {
	  if(argv[i][0]=='-') {
	    /* process options */
	    switch(argv[i][1]) {
	    case 'v':
		verbose = 1;
		break;
	    case 'o':
		if(argv[i][2]) outfile=argv[i]+2;
		else if(i + 1 < argc) outfile=argv[++i];
		else fprintf(stderr,"%s: missing output file\n",programname);
		break;
	    case 'X':
		extract=3;
		break;
	    case 'b':
	        base=NULL;
		switch(argv[i][2]) {
		case 't':
			tflag= 1;
			base=&tbase;
			break;
		case 'd':
			dflag= 1;
			base=&dbase;
			break;
		case 'b':
			bflag= 1;
			base=&bbase;
			break;
		case 'z':
			zflag= 1;
			base=&zbase;
			break;
		default:
			printf("Unknown segment type '%c' - ignored!\n", argv[i][2]);
			break;
		}
		if (base != NULL) {
			if(argv[i][3]) *base = atoi(argv[i]+3);
			else if(i + 1 < argc) *base = atoi(argv[++i]);
			else fprintf(stderr,"%s: missing address\n",programname);
		}
		break;
	    case 'x':		/* extract segment */
	        switch(argv[i][2]) {
		case 't':
			extract = 1;
			break;
		case 'd':
			extract = 2;
			break;
		case 'z':
		case 'b':
			printf("Cannot extract segment type '%c' - ignored!\n", argv[i][2]);
			break;
		default:
			printf("Unknown segment type '%c' - ignored!\n", argv[i][2]);
			break;
		}
		break;
	    default:
		fprintf(stderr,"%s: %s unknown option, use '-?' for help\n",programname,argv[i]);
		break;
	    }
	  } else {
	    struct stat fs;
	    file.fname=argv[i];
	    stat(argv[i], &fs);
	    file.fsize=fs.st_size;
	    file.buf=malloc(file.fsize);
	    if(!file.buf) {
	      fprintf(stderr,"Oops, no more memory!\n");
	      exit(1);
	    }
	    printf("reloc65: read file %s -> %s\n",argv[i],outfile);
	    fp = fopen(argv[i],"rb");
	    if(fp) {
	      n = fread(file.buf, 1, file.fsize, fp);
	      fclose(fp);
	      if((n>=file.fsize) && (!memcmp(file.buf, cmp, 5))) {
		mode=file.buf[7]*256+file.buf[6];
		if(mode & 0x2000) {
	          fprintf(stderr,"reloc65: %s: 32 bit size not supported\n", argv[i]);
		} else
		if(mode & 0x4000) {
	          fprintf(stderr,"reloc65: %s: pagewise relocation not supported\n", argv[i]);
		} else {
		  hlen = BUF+read_options(file.buf+BUF);
		  
		  file.tbase = file.buf[ 9]*256+file.buf[ 8];
		  file.tlen  = file.buf[11]*256+file.buf[10];
		  file.tdiff = tflag ? tbase - file.tbase : 0;

		  file.dbase = file.buf[13]*256+file.buf[12];
		  file.dlen  = file.buf[15]*256+file.buf[14];
		  file.ddiff = dflag ? dbase - file.dbase : 0;
		  if (extract == 3) {
		    if (dflag) {
	              fprintf(stderr,"reloc65: %s: Warning: data segment address overrides -X option\n", argv[i]);
		    } else {
		      dbase = file.tbase + file.tdiff + file.tlen;
		      file.ddiff = dbase - file.dbase;
		    }
		  }

		  file.bbase = file.buf[17]*256+file.buf[16];
		  file.blen  = file.buf[19]*256+file.buf[18];
		  file.bdiff = bflag ? bbase - file.bbase : 0;
		  if (extract == 3) {
		    if (bflag) {
	              fprintf(stderr,"reloc65: %s: Warning: bss segment address overrides -X option\n", argv[i]);
		    }  else {
		      bbase = file.dbase + file.ddiff + file.dlen;
		      file.bdiff = bbase - file.bbase;
		    }
		  }

		  file.zbase = file.buf[21]*256+file.buf[20];
		  file.zlen  = file.buf[23]*256+file.buf[22];
		  file.zdiff = zflag ? zbase - file.zbase : 0;

		  if (verbose) {
			printf("Relocating segments to:\n");
			printf("text segment @ $%04x - $%04x, %5d ($%04x) bytes, diff is %5d ($%04x)\n", 
				file.tbase + file.tdiff, file.tbase + file.tdiff + file.tlen, 
				file.tlen, file.tlen, file.tdiff, file.tdiff & 0xffff);
			printf("data segment @ $%04x - $%04x, %5d ($%04x) bytes, diff is %5d ($%04x)\n", 
				file.dbase + file.ddiff, file.dbase + file.ddiff + file.dlen,
				file.dlen, file.dlen, file.ddiff, file.ddiff & 0xffff);
			printf("bss  segment @ $%04x - $%04x, %5d ($%04x) bytes, diff is %5d ($%04x)\n", 
				file.bbase + file.bdiff, file.bbase + file.bdiff + file.blen,
				file.blen, file.blen, file.bdiff, file.bdiff & 0xffff);
			printf("zero segment @ $%04x - $%04x, %5d ($%04x) bytes, diff is %5d ($%04x)\n", 
				file.zbase + file.zdiff, file.zbase + file.zdiff + file.zlen,
				file.zlen, file.zlen, file.zdiff, file.zdiff & 0xffff);
		  }

		  /* pointer of position in file */
		  file.segt  = file.buf + hlen;
		  file.segd  = file.segt + file.tlen;
		  file.utab  = file.segd + file.dlen;

		  file.rttab = file.utab + read_undef(file.utab);
		  file.rdtab = reloc_seg(file.segt, file.tlen, file.rttab, 
					&file, extract);
		  file.extab = reloc_seg(file.segd, file.dlen, file.rdtab, 
					&file, extract);

		  reloc_globals(file.extab, &file);

		  if(tflag) {
			file.buf[ 9]= (tbase>>8)&255;
			file.buf[ 8]= tbase & 255;
		  }
  		  if(dflag) {
			file.buf[13]= (dbase>>8)&255;
			file.buf[12]= dbase & 255;
		  }
		  if(bflag) {
			file.buf[17]= (bbase>>8)&255;
			file.buf[16]= bbase & 255;
		  }
		  if(zflag) {
			file.buf[21]= (zbase>>8)&255;
			file.buf[20]= zbase & 255;
		  }

		  fp = fopen(outfile, "wb");
		  if(fp) {
		    switch(extract) {
		    case 0:	/* whole file */
		    	fwrite(file.buf, 1, file.fsize, fp);
			break;
		    case 1:	/* text segment */
			fwrite(file.segt, 1, file.tlen, fp);
			break;
		    case 2:	/* data segment */
			fwrite(file.segd, 1, file.dlen, fp);
			break;
		    case 3:	/* text+data */
			fwrite(file.segt, 1, file.tlen, fp);
			fwrite(file.segd, 1, file.dlen, fp);
			break;
		    }
		    fclose(fp);
		  } else {
	            fprintf(stderr,"reloc65: write '%s': %s\n", 
						outfile, strerror(errno));
		  }
		}
	      } else {
	        fprintf(stderr,"reloc65: %s: not an o65 file!\n", argv[i]);
		if(file.buf[0]==1 && file.buf[1]==8 && file.buf[3]==8) {
		  printf("%s: C64 BASIC executable (start address $0801)?\n", argv[i]);
		} else
		if(file.buf[0]==1 && file.buf[1]==4 && file.buf[3]==4) {
		  printf("%s: CBM PET BASIC executable (start address $0401)?\n", argv[i]);
		}
	      }
	    } else {
	      fprintf(stderr,"reloc65: read '%s': %s\n", 
						argv[i], strerror(errno));
	    }
	  }
	  i++;
	}
	exit(0);
}


int read_options(unsigned char *buf) {
	int c, l=0;

	c=buf[0];
	while(c && c!=EOF) {
	  c&=255;
	  l+=c;
	  c=buf[l];
	}
	return ++l;
}

int read_undef(unsigned char *buf) {
	int n, l = 2;

	n = buf[0] + 256*buf[1];
	while(n){
	  n--;
	  while(buf[l] != 0) {
		l++;
	  }
	  l++;
	}
	return l;
}

#define	reldiff(s)	(((s)==2)?fp->tdiff:(((s)==3)?fp->ddiff:(((s)==4)?fp->bdiff:(((s)==5)?fp->zdiff:0))))

unsigned char *reloc_seg(unsigned char *buf, int len, unsigned char *rtab, 
			file65 *fp, int undefwarn) {
	int adr = -1;
	int type, seg, old, new;
/*printf("tdiff=%04x, ddiff=%04x, bdiff=%04x, zdiff=%04x\n",
		fp->tdiff, fp->ddiff, fp->bdiff, fp->zdiff);*/
	while(*rtab) {
	  if((*rtab & 255) == 255) {
	    adr += 254;
	    rtab++;
	  } else {
	    adr += *rtab & 255;
	    rtab++;
	    type = *rtab & 0xe0;
	    seg = *rtab & 0x07;
/*printf("reloc entry @ rtab=%p (offset=%d), adr=%04x, type=%02x, seg=%d\n",rtab-1, *(rtab-1), adr, type, seg);*/
	    rtab++;
	    switch(type) {
	    case 0x80:	/* WORD - two byte address */
		old = buf[adr] + 256*buf[adr+1];
		new = old + reldiff(seg);
		buf[adr] = new & 255;
		buf[adr+1] = (new>>8)&255;
		break;
	    case 0x40:	/* HIGH - high byte of an address */
		old = buf[adr]*256 + *rtab;
		new = old + reldiff(seg);
		buf[adr] = (new>>8)&255;
		*rtab = new & 255;
		rtab++;
		break;
	    case 0x20:	/* LOW - low byt of an address */
		old = buf[adr];
		new = old + reldiff(seg);
		buf[adr] = new & 255;
		break;
	    }
	    if(seg==0) {
		/* undefined segment entry */
		if (undefwarn) {
	          fprintf(stderr,"reloc65: %s: Warning: undefined relocation table entry not handled!\n", fp->fname);
		}
		rtab+=2;
	    }
	  }
	}
	if(adr > len) {
	  fprintf(stderr,"reloc65: %s: Warning: relocation table entries past segment end!\n",
		fp->fname);
	  fprintf(stderr, "reloc65: adr=%x len=%x\n", adr, len);
	}
	return ++rtab;
}

unsigned char *reloc_globals(unsigned char *buf, file65 *fp) {
	int n, old, new, seg;

	n = buf[0] + 256*buf[1];
	buf +=2;

	while(n) {
/*printf("relocating %s, ", buf);*/
	  while(*(buf++));
	  seg = *buf;
	  old = buf[1] + 256*buf[2];
	  new = old + reldiff(seg);
/*printf("old=%04x, seg=%d, rel=%04x, new=%04x\n", old, seg, reldiff(seg), new);*/
	  buf[1] = new & 255;
	  buf[2] = (new>>8) & 255;
	  buf +=3;
	  n--;
	}
	return buf;
}

