/*
 * fatfs.c  -  Routines for reading a FAT filesystem
 *
 * Copyright (C) 2002-2007 Gero Kuhlmann   <gero@gkminix.han.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
 *  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.
 *
 * $Id: fatfs.c,v 1.16 2007/01/06 18:31:21 gkminix Exp $
 */

#define NEED_BINARY 1
#define NEED_TIME 1
#include <common.h>
#include <nblib.h>
#include "mknbi.h"
#include "dir.h"
#include "headers/fat.h"
#include "headers/boot.h"

#ifndef MKNBI_H_DOS
#error Included wrong header file
#endif



/*
 * Buffers for temporary holding the disk FAT
 */
static enum { TYPE_FAT12, TYPE_FAT16 } fattype;
static unsigned long fatsize;
static __u8 *fatbuf = NULL;



/*
 * Variables holding important values of a FAT filesystem, All size
 * values here are in bytes, not sectors.
 */
static int fatfile = -1;
static unsigned long clustsize;
static unsigned long rootsize;
static unsigned long dataoffset;
static unsigned long rootoffset;
static hchar_t volumename[12] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };





/***************************************************************************

			Routines to read a file sector

 ***************************************************************************/

/* Current file position */
struct filepos {
	unsigned long cursect;
	unsigned long curclust;
	unsigned long cursize;
};

/* Current disk position */
static unsigned long lastpos = 0L; 



/*
 * Read one sector from FAT image file
 */
static void readsect __F((buf, offset), __u8 *buf AND unsigned long offset)
{
  if (offset != lastpos) {
	if ((unsigned long)lseek(fatfile, offset, 0) != offset) {
		prnerr("unable to seek within DOS image file");
		nbexit(EXIT_SEEK);
	}
  }
  if (nbread(buf, SECTSIZE, fatfile) != SECTSIZE) {
	prnerr("unexpected end of DOS image file/device");
	nbexit(EXIT_DOS_RDEOF);
  }
  lastpos = offset + SECTSIZE;
}



/*
 * Open a file/directory
 */
static struct filepos *openfile __F((cluster, size),
				unsigned long cluster AND
				unsigned long size)
{
  struct filepos *fp;

  fp = (struct filepos *)nbmalloc(sizeof(struct filepos));
  fp->curclust = cluster;
  fp->cursize = size;
  fp->cursect = 0;
  return(fp);
}



/*
 * Close a file/directory
 */
static void closefile __F((fp), struct filepos *fp)
{
  if (fp != NULL)
	free(fp);
}



/*
 * Read a sector from a cluster, returns FALSE if end of file
 */
static int readclust __F((fp, buf), struct filepos *fp AND __u8 *buf)
{
  unsigned long offset;
  unsigned long entry;

  /* Check if at end of file */
  if (((fp->cursize > 0) && ((fp->cursect * SECTSIZE) >= fp->cursize)) ||
      ((fattype == TYPE_FAT12) && (fp->curclust >= 0x0ff0)) ||
      ((fattype == TYPE_FAT16) && (fp->curclust >= 0xfff0)))
	return(FALSE);

  /* Read requested sector */
  if (fp->curclust > 0) {
	offset = (fp->curclust - FIRST_CLUSTER) * clustsize + dataoffset;
	offset += (fp->cursect * SECTSIZE) % clustsize;
	readsect(buf, offset);
  } else {
	offset = rootoffset + (fp->cursect * SECTSIZE);
	readsect(buf, offset);
  }

  /* Advance pointers to next sector */
  fp->cursect++;
  if ((fp->curclust > 0) &&
      (((fp->cursect * SECTSIZE) % clustsize) == 0)) {
	if (fattype == TYPE_FAT12) {
		entry  = get_word(*((__u16 *)(fatbuf +
					(fp->curclust * 12) / 8)));
		entry |= get_word(*((__u16 *)(fatbuf +
					(fp->curclust * 12) / 8 + 2))) << 16;
		entry >>= (fp->curclust * 12) % 8;
		entry &= 0x0fff;
	} else {
		entry  = get_word(*((__u16 *)
					(fatbuf + (fp->curclust * 2))));
		entry &= 0xffff;
	}
	fp->curclust = entry;
  }
  return(TRUE);
}





/***************************************************************************

			Routines to read directory tree

 ***************************************************************************/

/* Directory buffers */
struct dirpos {
	__u8            dirbuf[SECTSIZE];
	unsigned int    diroffset;
	struct filepos *fp;
};



/*
 * Open directory
 */
static struct dirpos *fatopendir __F((dirp), struct dos_dir *dirp)
{
  struct dirpos *dp;

  dp = (struct dirpos *)nbmalloc(sizeof(struct dirpos));
  if (dirp != NULL)
	dp->fp = openfile(get_word(dirp->cluster), get_long(dirp->size));
  else
	dp->fp = openfile(0, rootsize);
  dp->diroffset = SECTSIZE;
  return(dp);
}



/*
 * Close directory
 */
static void fatclosedir __F((dp), struct dirpos *dp)
{
  if (dp != NULL)
	free(dp);
}



/*
 * Get next directory entry, returns NULL if nothing to read
 */
static struct dos_dir *getnextdir __F((dp), struct dirpos *dp)
{
  struct dos_dir *dirp;

  while (TRUE) {
	/* Read next directory sector */
	if (dp->diroffset >= SECTSIZE) {
		if (!readclust(dp->fp, dp->dirbuf))
			return(NULL);
		dp->diroffset = 0;
	}

	/* Check that directory entry is not empty */
	dirp = (struct dos_dir *)&(dp->dirbuf[dp->diroffset]);
	dp->diroffset += sizeof(struct dos_dir);
	if (dirp->name[0] != 0 && dirp->name[0] != 0xe5)
		break;
  }
  return(dirp);
}



/*
 * Compute checksum of short file name
 */
static int getchksum __F((dirp), const struct dos_dir *dirp)
{
  int chksum = 0;
  int i;

  for (i = 0; i < 8; i++) {
	chksum = (chksum >> 1) | ((chksum & 0x01) << 7);
	chksum = (chksum + dirp->name[i]) & 0xff;
  }
  for (i = 0; i < 3; i++) {
	chksum = (chksum >> 1) | ((chksum & 0x01) << 7);
	chksum = (chksum + dirp->ext[i]) & 0xff;
  }
  return(chksum);
}



/*
 * Copy file name from directory entry into directory record.
 * We do this byte by byte because on the host system, characters
 * don't necessarily need to be encoded as with DOS.
 */
static inline void namecopy __F((dest, src, len),
				hchar_t *dest AND
				const __u8 *src AND
				int len)
{
  register int i;

  for (i = 0; i < len; i++)
	*dest++ = chartohost(*src++);
}



/*
 * Read in the complete directory structure
 */
static struct dir_struct *rdfatdir __F((src_dirp, src_lfn_name, src_lfn_num),
				struct dos_dir *src_dirp AND
				ucs_t *src_lfn_name AND
				int src_lfn_num)
{
  struct dirpos *dp;
  struct dos_dir *dirp;
  struct dir_struct *dsp;
  struct dir_struct *tmpdsp;
  struct file_struct *fsp;
  ucs_t *tmp_name;
  ucs_t *lfn_name;
  __u16 *tmpptr;
  int lfn_size = 0;
  int lfn_chksum = 0;
  int i, j;

  /* Initialize temporary LFN buffer */
  tmp_name = (ucs_t *)nbmalloc(LFN_CHARS * 64 * sizeof(ucs_t));
  memset(tmp_name, 0xff, LFN_CHARS * 64 * sizeof(ucs_t));

  /* Initialize directory structure */
  dsp = (struct dir_struct *)nbmalloc(sizeof(struct dir_struct));
  if (src_dirp != NULL) {
	namecopy(dsp->name.name, src_dirp->name, 8);
	namecopy(dsp->name.ext, src_dirp->ext, 3);
	dsp->name.lfn_name = src_lfn_name;
	dsp->name.lfn_num = src_lfn_num;
	dsp->attrib = src_dirp->attrib;
	dsp->time = get_word(src_dirp->time);
	dsp->date = get_word(src_dirp->date);
	dsp->src.cluster = get_word(src_dirp->cluster);
  } else {
	dsp->attrib = ATTR_DIR;
	gettime(time(NULL), &(dsp->date), &(dsp->time));
  }
  dsp->subdirnum = 0;
  dsp->filenum = 0;
  dsp->lfnnum = 0;
  dsp->totsize = 0L;
  dsp->subdirs = NULL;
  dsp->files = NULL;
  dsp->next = NULL;

  /* Read in whole directory */
  dp = fatopendir(src_dirp);
  while ((dirp = getnextdir(dp)) != NULL) {
	if ((bytecmp(".       ", dirp->name, 8) ||
	     bytecmp("..      ", dirp->name, 8)) &&
	     bytecmp("   ", dirp->ext, 3))
		continue;
	if (dirp->attrib == ATTR_LFN) {
		struct lfn_dir *lfnp = (struct lfn_dir *)dirp;

		/*
		 * Check that the checksum is the same as of the preceding
		 * long file name entry.
		 */
		if (lfn_size == 0)
			lfn_chksum = lfnp->checksum;
		else if (lfn_chksum != lfnp->checksum) {
			lfn_size = 0;
			continue;
		}

		/*
		 * Copy long file name characters into temporary buffer
		 */
		i = ((lfnp->sequence & LFN_SEQ_MASK) - 1) * LFN_CHARS;
		tmpptr = lfnp->first_part;
		for (j = 0; j < LFN_CHARS; j++) {
			tmp_name[i + j] = get_word(*tmpptr);
			if (tmp_name[i + j] == 0)
				break;
			lfn_size++;
			if (j == 5)
				tmpptr = lfnp->second_part;
			else if (j == 11)
				tmpptr = lfnp->third_part;
			else
				tmpptr++;
		}
		continue;
	} else if ((lfn_size > 0) && (lfn_chksum != getchksum(dirp))) {
		/*
		 * If the checksum of the directory entry name doesn't
		 * fit to the checksum of the preceding long filename
		 * records, don't use a long file name for this directory
		 * entry.
		 */
		lfn_size = 0;
	}

	lfn_name = NULL;
	if (lfn_size > 0) {
		lfn_size = roundup(lfn_size, LFN_CHARS);
		lfn_name = (ucs_t *)nbmalloc(lfn_size * sizeof(ucs_t));
		memcpy(lfn_name, tmp_name, (lfn_size * sizeof(ucs_t)));
		memset(tmp_name, 0xff, LFN_CHARS * 64 * sizeof(ucs_t));
	}
	if (!(dirp->attrib & ATTR_DIR) &&
	    !(dirp->attrib & ATTR_LABEL)) {
		/* Handle entry for ordinary files */
		fsp = (struct file_struct *)nbmalloc (sizeof(struct file_struct));
		namecopy(fsp->name.name, dirp->name, 8);
		namecopy(fsp->name.ext, dirp->ext, 3);
		fsp->name.lfn_name = lfn_name;
		fsp->name.lfn_num = howmany(lfn_size, LFN_CHARS);
		fsp->attrib = dirp->attrib;
		fsp->time = get_word(dirp->time);
		fsp->date = get_word(dirp->date);
		fsp->src.cluster = get_word(dirp->cluster);
		fsp->size = get_long(dirp->size);
		fsp->next = dsp->files;
		dsp->totsize += howmany(fsp->size, 1024);
		dsp->lfnnum += fsp->name.lfn_num;
		dsp->files = fsp;
		dsp->filenum++;
	} else if (!(dirp->attrib & ATTR_LABEL)) {
		/* Handle directory entry */
		tmpdsp = rdfatdir(dirp, lfn_name, howmany(lfn_size, LFN_CHARS));
		tmpdsp->next = dsp->subdirs;
		dsp->totsize += tmpdsp->totsize;
		dsp->lfnnum += tmpdsp->name.lfn_num;
		dsp->subdirs = tmpdsp;
		dsp->subdirnum++;
	} else {
		/* Handle entry for volume name */
		if (lfn_name != NULL) {
			/* For labels just eat long file name */
			free(lfn_name);
			lfn_name = NULL;
		}
		if (volumename[0] == 0) {
			namecopy(&(volumename[0]), dirp->name, 8);
			namecopy(&(volumename[8]), dirp->ext, 3);
		}
	}
	lfn_size = 0;
  }
  fatclosedir(dp);
  free(tmp_name);

  /* Add size of directory to total size, except for root directory  */
  if (src_dirp != NULL)
	dsp->totsize += howmany(DIR_ENTRIES(dsp) * sizeof(struct dos_dir),
									1024);
  return(dsp);
}





/***************************************************************************

			Routines to open and close the image file

 ***************************************************************************/

/*
 * Open a FAT file system image/device
 */
void fatopen __F((sysentry, filesys),
				struct sysdef *sysentry AND
				struct fs_struct *filesys)
{
  static __u8 buf[SECTSIZE];
  struct boot_record *bootrec;
  unsigned long total_sects;
  unsigned long fat_offset;
  unsigned int sect_per_fat;
  unsigned int fat_num;
  unsigned int i, j;

  /* Just for safety */
  assert(filesys != NULL);

  /*
   * Open FAT image file or device and read the boot sector.
   */
  if ((fatfile = open(sysentry->rdname, O_RDONLY | O_BINARY)) < 0) {
	prnerr("unable to open DOS image file %s", sysentry->rdname);
	nbexit(EXIT_DOS_RDOPEN);
  }
  readsect(buf, 0);
  if (getval(*((__u16 *)&(buf[BOOT_SIG_OFS]))) != htot(BOOT_SIG)) {
	prnerr("DOS image file %s has no boot signature", sysentry->rdname);
	nbexit(EXIT_DOS_NOBOOT);
  }
  bootrec = (struct boot_record *)buf;

  /*
   * Check that the boot record of the image file really contains a FAT
   * filesystem and read some important values necessary for accessing the
   * filesystem lateron.
   */
  if (bytecmp(BOOT_FAT12_NAME, bootrec->fat_name, sizeof(bootrec->fat_name)))
	fattype = TYPE_FAT12;
  else if (bytecmp(BOOT_FAT16_NAME, bootrec->fat_name, sizeof(bootrec->fat_name)))
	fattype = TYPE_FAT16;
  else {
	prnerr("DOS image file has invalid filesystem");
	nbexit(EXIT_DOS_INVFS);
  }
  if (getval(bootrec->bytes_per_sect) != htot(SECTSIZE)) {
	prnerr("DOS image file has wrong sector size");
	nbexit(EXIT_DOS_SECTSIZE);
  }
  fat_num = bootrec->fat_num;
  if ((fat_num == 0) || (fat_num > 2)) {
	prnerr("DOS image file has invalid number of FATs");
	nbexit(EXIT_DOS_FATNUM);
  }
  fat_offset = get_word(bootrec->reserved_sect) * SECTSIZE;
  total_sects = get_word(bootrec->sect_num);
  if (total_sects == 0)
	total_sects = get_long(bootrec->sect_num_32);
  if (total_sects < (MIN_RDSIZE * SECTS_PER_KB)) {
	prnerr("DOS image file has invalid number of sectors");
	nbexit(EXIT_DOS_INVSIZE);
  } else if (total_sects > ((ULONG_MAX / (unsigned long)SECTSIZE) - MAX_SECTS)) {
	prnerr("DOS image file has too many sectors");
	nbexit(EXIT_DOS_INVSIZE);
  }
  sect_per_fat = get_word(bootrec->sect_per_fat);
  if (sect_per_fat == 0) {
	prnerr("DOS image file has invalid FAT size");
	nbexit(EXIT_DOS_INVFAT);
  }
  fatsize = sect_per_fat * SECTSIZE;
  clustsize = bootrec->sect_per_cluster * SECTSIZE;
  rootsize = roundup(get_word(bootrec->dir_num) * sizeof(struct dos_dir),
								SECTSIZE);
  rootoffset = fat_offset + (fat_num * fatsize);
  dataoffset = rootoffset + rootsize;

  /*
   * Prepare the boot block by reading all reserved sectors. This is necesary
   * so that any additional boot sectors get copied unmodified.
   */
  filesys->boot_size = fat_offset;
  filesys->boot_block = (__u8 *)nbmalloc(filesys->boot_size);
  memcpy(filesys->boot_block, buf, SECTSIZE);
  for (i = SECTSIZE; i < filesys->boot_size; i += SECTSIZE)
	readsect(&(filesys->boot_block[i]), i);

  /*
   * Read first FAT. Note that this will destroy the buffer which
   * contains the boot block!
   */
  fatbuf = (__u8 *)nbmalloc(fatsize);
  for (i = 0; i < sect_per_fat; i++)
	readsect(&fatbuf[i * SECTSIZE], ((i * SECTSIZE) + fat_offset));
  if (fatbuf[0] != bootrec->media_id) {
	prnerr("DOS image file has invalid FAT");
	nbexit(EXIT_DOS_INVFAT);
  }
  for (j = 1; j < fat_num; j++) {
	fat_offset += sect_per_fat * SECTSIZE;
	for (i = 0; i < sect_per_fat; i++) {
		readsect(buf, ((i * SECTSIZE) + fat_offset));
		if (memcmp(buf, &fatbuf[i * SECTSIZE], SECTSIZE)) {
			prnerr("FAT copies in DOS image file differ");
			nbexit(EXIT_DOS_INVFAT);
		}
	}
  }

  /* Read root directory recursively */
  filesys->root_dir = rdfatdir(NULL, NULL, 0);

  /* Set volume name if available */
  if (sysentry->volumename == NULL && volumename[0] != 0) {
	size_t dlen, clen;
	char *cp;

	for (dlen = 0, i = 0; i < 12; i++)
		dlen += charlen(volumename[i]);
	sysentry->volumename = cp = (char *)nbmalloc(dlen + 1);
	for (i = 0; i < 12; i++) {
		clen = savechar(volumename[i], cp, dlen);
		dlen -= clen;
		cp += clen;
	}
	volumename[0] = 0;
  }
}



/*
 * Close FAT image file or device
 */
void fatclose __F((filesys), struct fs_struct *filesys)
{
  assert(filesys != NULL);
  if (fatfile >= 0)
	close(fatfile);
  if (fatbuf != NULL)
	free(fatbuf);
  if (filesys->boot_block != NULL) {
	free(filesys->boot_block);
	filesys->boot_block = NULL;
	filesys->boot_size = 0;
  }
}



/*
 * Copy a file from the FAT image into the output file
 */
unsigned long fatcopy __F((clustsize, handle, fsp),
				int clustsize AND
				int handle AND
				const struct file_struct *fsp)
{
  struct filepos *fp;
  __u8 *buf;
  unsigned long num;
  unsigned long writecnt = 0;
  int eof = FALSE;
  int i;

  buf = (__u8 *)nbmalloc(clustsize);
  fp = openfile(fsp->src.cluster, fsp->size);
  for (num = 0; num < fsp->clustnum; num++) {
	memzero(buf, clustsize);
	for (i = 0; !eof && (i < clustsize); i += SECTSIZE)
		eof = !readclust(fp, &(buf[i]));
	writecnt += nbwrite(buf, clustsize, handle);
  }
  closefile(fp);
  free(buf);
  return(writecnt);
}

