/*
 * Convert a file containing a raw dump of a Tektronix fbr floppy image to a tar archive
 * with an embedded content report record in "TarXc" format
 *
 * options
 *  -b file  use <file> for lot information
 *  -i       ignore checksum errors in directory entries
 *  -l       generate table of contents to stdout instead of a tarball
 *
 * 1.0 20120521 aek
 */

#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <string.h>
//#include "md5.h"

#define UID_NOBODY 32767
#define GID_NOBODY 32767

/*
 * Tek fbr disks are either single or double sided. The first track on side 0
 * has 26 sectors of 128 bytes. The rest of the disk is 26 sectors of 256 bytes
 * The first four 128 byte sectors are ignored.
 *
 * fbr directory entry
 *
 * The first directory entry is a label entry for the entire archive
 *   path A comment about this archive.
 *   mode Set by fbr to a file readable/writeable/executable by all. Other-
 *        wise unused.
 *   uid  The userID of the user who created this archive.
 *   gid  The groupID of the same.
 *   size The total number of bytes in the data area of the archive.This
 *        field  in  combination  with  the fblk field, records the size of
 *        both the directory area and the entire archive.
 *   acct The date-time that this archive was  created  -  not its  access
 *        time.
 *   modt The date-time that this archive was last altered.
 *   fblk The block number of the first archive block following the direc-
 *        tory area.
 */

typedef struct {			// originally 128 bytes long
	char path[106];			// pathname (or label comment)
	unsigned short mode;		// unix file mode
	short uid;			// unix UID
	short gid;			// unix GID
	unsigned long fsize;		// file size
	time_t access_t;		// time last accessed (or when archive was created)
	time_t mod_t;			// time last modified
	unsigned short fblk;		// file starting block
	char unused;
	char cksum;			// fbr header checksum, byte initialized to -1
					// extra fields added by this program
	char ccksum;			// computed checksum
	char valid;			// checksum was valid
	char *f_data;			// pointer to the contents of the file
	int f_len;			// real length padded to 512 byte boundary
	int nam_len;			// length of the file path
	char md5[33];			// MD5 hash of the contents of the file
	long offset;			// offset to the tar file header in the tarball
} fbrdir;

fbrdir d_tbl[0x3f * 4];			// maximum number of directory entires on DS/DD disk
fbrdir d_lab;				// the fbr label block

/*
 * A POSIX tar file header. Currently the program does not deal with long pathnames
 */
typedef struct {			// a V7 tar header, 512 bytes long
	char path[100];			// file path, currently limited to original V7 length
	char mode[8];			// all numeric data represented as octal
	char uid[8];			// null-terminated strings with leading zeros
	char gid[8];
	char size[12];
	char mtime[12];
	char cksum[7];			// checksum over all 512 bytes initialized to ' '
	char ftyp[2];			// POSIX file type
	char link[100];
	char ustar[8];			// POSIX identifier "ustar  "
	char unam[32];			// POSIX UID name string
	char gnam[32];			// POSIX GID name string
	char unused[180];
} tarlbl;

tarlbl t_lab;

long bytecount = 0;

struct tm time_s;
char time_str[50];
FILE *toc_file;

/*
 * fetch a byte from stdin. if we've reached EOF, exit with an error
 */
unsigned char get_stdin(){

	if(feof(stdin)){
	 fprintf(stderr,"Error: EOF on stdin after byte %d\n", bytecount);
	 exit(EXIT_FAILURE);
	}
	else{
	 bytecount++;
	 return(getchar());
	}

}

/*
 * skip n bytes from stdin. If we've reached EOF, exit with an error
 */
void skip_stdin(int count) {
	while(count--){
	 if(feof(stdin)){
	  fprintf(stderr,"Error: EOF on stdin after byte %d\n", bytecount);
	  exit(EXIT_FAILURE);
	 }
	 else{
	  bytecount++;
	  getchar();
	 }
	}
}

/*
 * little-endian byte munging with checksum accumulation
 */
unsigned int get_16(char *cksum){
 unsigned int i;
 unsigned char c;
 c = get_stdin(); *cksum += c; i  = c;
 c = get_stdin(); *cksum += c; i |= (c << 8);
 return(i);
}

unsigned long get_32(char *cksum){
 unsigned long l;
 unsigned char c;
 c = get_stdin(); *cksum += c; l  = c << 16;
 c = get_stdin(); *cksum += c; l |= c << 24;
 c = get_stdin(); *cksum += c; l |= c;
 c = get_stdin(); *cksum += c; l |= c << 8;
 return(l);
}

/*
 * create fbr label record in the TOC
 */
print_label(fbrdir *p){

	fprintf(toc_file,"| Label:                    | Mode: %06o | UID: %-02d | GID: %-02d | Sz: %-6d | Sbk: %-4X | Cks: %02X ",
	 p->mode, p->uid, p->gid, p->fsize, p->fblk, (unsigned char)p->cksum, p->ccksum, p->path);
	
	time_s = *gmtime(&p->access_t);
	strftime(time_str, sizeof(time_str), "%\"%Y-%m-%d %H:%M:%S%\"", &time_s);
	fprintf(toc_file,"| Cre: %s ", time_str);

	time_s = *gmtime(&p->mod_t);
	strftime(time_str, sizeof(time_str), "%\"%Y-%m-%d %H:%M:%S%\"", &time_s);
	fprintf(toc_file,"| Mod: %s ", time_str);
	
	fprintf(toc_file,"| Nam: %\"%s%\"\n", p->path);

}

/*
 * create a file information record in the TOC
 */
print_dir(int fnum, fbrdir *p){

	if(p->mode & 0040000)
	 fprintf(toc_file, "| Dir:  ");
	if(p->mode & 0100000)
	 fprintf(toc_file, "| File: ");
	fprintf(toc_file, "%-3d | Offs: %-7d | Mode: %06o | UID: %-02d | GID: %-02d | Sz: %-6d | Sbk: %-4X | Cks: %02X ",
	 fnum, p->offset, p->mode, p->uid, p->gid, p->fsize, p->fblk, (unsigned char)p->cksum, p->ccksum, p->path);

	time_s = *gmtime(&p->access_t);
	strftime(time_str, sizeof(time_str), "%\"%Y-%m-%d %H:%M:%S%\"", &time_s);
	fprintf(toc_file,"| Acc: %s ", time_str);

	time_s = *gmtime(&p->mod_t);
	strftime(time_str, sizeof(time_str), "%\"%Y-%m-%d %H:%M:%S%\"", &time_s);
	fprintf(toc_file,"| Mod: %s ", time_str);
	fprintf(toc_file,"| MD5: %s ", p->md5);
	fprintf(toc_file,"| Path: %\"%s%\"\n",p->path);
}

/*
 * Analyze all the raw bytes of a fbr disk image, then use the collected
 * information to generate a tarball with an additional file on the front containing
 * a summary of the fbr contents
 */
main(int argc, char *argv[]){

	unsigned int i, j, k;
	unsigned char c;
	fbrdir *p;
	char *cp;
	unsigned int cksum;
	int max_fblks;
	int actual_fblks = 0;
	int longest_path = 0;
	char *prefix_string;
	char *null_hash = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
	int toc_filesize;
	struct timeval rawtime;
	struct stat statbuf;
	
	char buf[100];

	/*
	 * create a temporary file for the TOC to live in
	 */
	 
	gettimeofday(&rawtime);
	time_s = *gmtime(&rawtime.tv_sec);

	if((toc_file = tmpfile()) == 0){
	 fprintf(stderr,"Error: could not open temp file\n");
	 exit(EXIT_FAILURE);
	}
	
	fprintf(toc_file,"TarXc_1.0\n");
	strftime(buf, sizeof(buf), "%\"%Y-%m-%d %X %Z\"", &time_s);
	fprintf(toc_file,"| Created: %s\n", buf);
	fprintf(toc_file,"| Format: %\"Tektronix fbr%\"\n");

	skip_stdin(512);	// first 512 bytes unused
	
	/*
	 * fetch the label block
	 */
	p = &d_lab;
	p->ccksum = -1;
	for(i=0; i<106; i++){
	 c = get_stdin();
	 p->ccksum += c;
	 p->path[i] = c;
	}
	p->mode = get_16(&p->ccksum);
	p->uid = get_16(&p->ccksum);
	p->gid = get_16(&p->ccksum);
	p->fsize = get_32(&p->ccksum);
	p->access_t = get_32(&p->ccksum);
	p->mod_t = get_32(&p->ccksum);
	p->fblk = get_16(&p->ccksum);
	p->unused = get_stdin();
	p->ccksum += p->unused;
	p->cksum = get_stdin();
	
	if(p->cksum == p->ccksum)
	 p->valid = 1;
	else{
	 fprintf(stderr,"Error: Label block cksum failed %x %x\n", p->cksum, p->ccksum);
	 exit(EXIT_FAILURE);
	}
	
	print_label(p);					// generate TOC label entry
		 
	/*
	 * create a path prefix string based on the current time
	 */
	asprintf(&prefix_string, "./FBR_%06X%04X/",rawtime.tv_sec,rawtime.((tv_usec>>10) & 0xffff));

	switch(p->fblk){
	 case 0x20:
	  max_fblks = (0x20 * 4) - 5;
	  break;
	 case 0x3f:
	  max_fblks = (0x3f * 4) - 5;
	  break;
	 default:
	  fprintf(stderr,"Error: first data block not at 0x20 or 0x3f\n");
	  exit(EXIT_FAILURE);
	}

	for(i = 0, p = d_tbl; i < max_fblks; i++, p++){
	 p->ccksum = -1;
	 for(j = 0; j < 106; j++){
	  c = get_stdin();
	  if(c) p->f_len++;
	  p->ccksum += c;
	  if((c < ' ')&&(c != 0)) c = 'x';		// convert non-printing char in path
	  p->path[j] = c;
	 }
	 if(longest_path < p->f_len) longest_path = p->f_len;
	 p->mode = get_16(&p->ccksum);
	 p->uid = get_16(&p->ccksum);
	 p->gid = get_16(&p->ccksum);
	 p->fsize = get_32(&p->ccksum);
	 p->access_t = get_32(&p->ccksum);
	 p->mod_t = get_32(&p->ccksum);
	 p->fblk = get_16(&p->ccksum);
	 p->unused = get_stdin();
	 p->ccksum += p->unused;
	 p->cksum = get_stdin();
	 
	 strcpy(p->md5, null_hash);
	
	 if(p->cksum == p->ccksum) p->valid = 1;
	 else{
	  fprintf(stderr,"Error: directory block %d cksum failed %x %x Offset %x\n",j , p->cksum, p->ccksum, bytecount);
	  exit(EXIT_FAILURE);
	 }
	
	 if(p->fblk != 0) actual_fblks++;
	}
	
	for(i = 0, p = d_tbl; i < actual_fblks; i++, p++){
	 if(p->fblk){
	  print_dir(i+1, p);
	 }
	}
	
	/*
	 * We have all the directories, walk the rest of the file and create content records.
	 * It would be easy, except that the starting blocks skip around on the disk, so we have
	 * to find the directory entry that matches how far we've progressed in the image file
	 */

	for(i = 0; i < actual_fblks; i++ ){		// walk the directory table

	 for(j = 0, p = d_tbl; j < actual_fblks; j++, p++){
	  if((p->fblk << 9) == bytecount) break;
	 }

	 if(j >= actual_fblks){
	  fprintf(stderr,"Error: input offset %d (blk %x) doesn't match a file starting block number\n", bytecount, bytecount >> 9);
	  exit(EXIT_FAILURE);
	 }

	 if(p->fsize % 512)				// if file length isn't a multiple of 512
	  j = p->fsize + (512 - (p->fsize %512));	// pad file size to 512 byte boundary
	 else
	  j = p->fsize;
	 if(j == 0)					// empty files take up one block
	  j = 512;
	 p->f_len = j;					// remember how much memory is malloc'ed
	 p->f_data = malloc(j);				// wire the buffer to the directory entry


	 for(j = 0; j < p->fsize; j++){			// copy the data
	  p->f_data[j] = get_stdin();
	 }

	 while(j < p->f_len){
	   get_stdin();					// ignore trailing stuff from the disk image and
	   p->f_data[j++] = 0;				// zero fill local buffer to block boundary
	 }

	/*
	 * calculate md5sum of actual contents of the file
	 */


	}
	
//	printf("byte count %d, longest pathname %d\n", bytecount, longest_path);
	if(longest_path >= (100 - strlen(prefix_string))){
	 fprintf(stderr,"Error: longest path %d, too long for V7 tar\n", longest_path);
	 exit(EXIT_FAILURE);
	}
	
	/*
	 * now build the tarball, starting with the accumulated file information
	 * pathnames have ./{uniqueid}/ prepended to them.
	 */

	toc_filesize = ftell(toc_file);			// calculate size of TOC file
	for(i = 512 - (toc_filesize % 512); i; i--)	// pad TOC to 512 byte boundary
	 fputc('\0', toc_file);

	rewind(toc_file);				// TOC file ready to go
	
	/*
	 * create the tar header for the TOC file
	 */

	memset(&t_lab,0 , sizeof(tarlbl));
	sprintf(t_lab.path,"%s__TarXc_TOC.txt",prefix_string);
	sprintf(t_lab.mode,"%07o", 0444);
	sprintf(t_lab.uid,"%07o", UID_NOBODY);
	sprintf(t_lab.gid,"%07o", GID_NOBODY);
	sprintf(t_lab.size,"%011o", toc_filesize);
	sprintf(t_lab.mtime, "%011o", rawtime.tv_sec);
	memset(&t_lab.cksum, 0x20, sizeof(t_lab.cksum)); // ENTIRE field set to 0x20
	sprintf(t_lab.ftyp, "%2d", 0);
	sprintf(t_lab.ustar, "ustar  ");
	sprintf(t_lab.unam, "nobody");
	sprintf(t_lab.gnam, "nobody");

	cp = &t_lab.path[0];
	cksum = 0;
	for(i=0; i<512; i++)
	 cksum += *cp++;
	sprintf(t_lab.cksum, "%06o", cksum);
	
	cp = &t_lab.path[0];
	for(i=0; i<512; i++)
	 putchar(*cp++);				// push out the TOC header

	for(i = toc_filesize + (512 - (toc_filesize % 512)); i; i--)
	 putchar(fgetc(toc_file));			// push out the TOC contents
	 
	/*
	 * walk the file list, creating all the file entries
	 */
	p = d_tbl;
	for(i = 0, p=d_tbl; i < actual_fblks; i++, p++ ){
	  memset(&t_lab,0 , sizeof(tarlbl));
	  sprintf(t_lab.path,"%s%s",prefix_string,p->path);
	  sprintf(t_lab.mode,"%07o", p->mode);
	  sprintf(t_lab.uid,"%07o", p->uid);
	  sprintf(t_lab.gid,"%07o", p->gid);
	  sprintf(t_lab.size,"%011o", p->fsize);
	  sprintf(t_lab.mtime, "%011o", p->mod_t);
	  memset(&t_lab.cksum, 0x20, sizeof(t_lab.cksum)); // ENTIRE field set to 0x20
	  if(p->mode & 0100000)
	   sprintf(t_lab.ftyp, "%2d", 0);
	  if(p->mode & 0040000)
	   sprintf(t_lab.ftyp, "%2d", 5);
	  sprintf(t_lab.ustar, "ustar  ");
//	  sprintf(t_lab.unam, "");
//	  sprintf(t_lab.gnam, "");

	  cp = &t_lab.path[0];
	  cksum = 0;
	  for(j=0; j<512; j++)
	   cksum += *cp++;
	  sprintf(t_lab.cksum, "%06o", cksum);
	
	  cp = &t_lab.path[0];
	  for(j=0; j<512; j++)
	   putchar(*cp++);				// push out the file contents
	   if(p->fsize){
	    cp = p->f_data;
	    for(j = p->f_len; j; j--)
	     putchar(*cp++);
	   }
	}


}
