/* geometry.c  -  Device and file geometry computation */

/* Written 1992-1994 by Werner Almesberger */


#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <dirent.h>
#include <limits.h>
#include <sys/stat.h>
#include <sys/ioctl.h>

#include <linux/fs.h>
#include <linux/fd.h>
#include <linux/hdreg.h>

#include "config.h"
#include "lilo.h"
#include "common.h"
#include "device.h"
#include "geometry.h"


typedef struct _st_buf {
    struct _st_buf *next;
    struct stat st;
} ST_BUF;

typedef struct _dt_entry {
    int device,bios;
    int sectors,heads,cylinders;
    int start;
    struct _dt_entry *next;
} DT_ENTRY;


static DT_ENTRY *disktab = NULL;


void geo_init(char *name)
{
    FILE *file;
    char line[MAX_LINE+1];
    char *here;
    DT_ENTRY *entry;
    int items;

    if (name) {
	if ((file = fopen(name,"r")) == NULL)
	    die("open %s: %s",name,strerror(errno));
    }
    else if ((file = fopen(DFL_DISKTAB,"r")) == NULL) return;
    while (fgets(line,MAX_LINE,file)) {
	if (here = strchr(line,'\n')) *here = 0;
	if (here = strchr(line,'#')) *here = 0;
	if (strspn(line," \t") != strlen(line)) {
	    entry = alloc(sizeof(DT_ENTRY));
	    items = sscanf(line,"0x%x 0x%x %d %d %d %d",&entry->device,
	      &entry->bios,&entry->sectors,&entry->heads,&entry->cylinders,
	      &entry->start);
	    if (items == 5) entry->start = -1;
	    if (items < 5)
		die("Invalid line in %s:\n\"%s\"",name ? name : DFL_DISKTAB,
		  line);
	    entry->next = disktab;
	    disktab = entry;
	}
    }
    (void) fclose(file);
}


static int last_dev(ST_BUF *next,char *parent,int major,int increment)
{
    DIR *dp;
    struct dirent *dir;
    char name[PATH_MAX+1],buff;
    ST_BUF st,*walk;
    int max,this;
    int fd;

    st.next = next;
    max = 0;
    if ((dp = opendir(parent)) == NULL)
	die("opendir %s: %s",parent,strerror(errno));
    while (dir = readdir(dp)) {
	sprintf(name,"%s/%s",parent,dir->d_name);
	if (stat(name,&st.st) >= 0) {
	    if (S_ISBLK(st.st.st_mode) && MAJOR(st.st.st_rdev) == major &&
	      (MINOR(st.st.st_rdev) & (increment-1)) == 0) {
		this = MINOR(st.st.st_rdev)/increment+1;
		if (this > max)
		    if ((fd = open(name,O_RDONLY)) >= 0) {
			if (read(fd,&buff,1) == 1) max = this;
			(void) close(fd);
		    }
	    }
	    if (S_ISDIR(st.st.st_mode) && strcmp(dir->d_name,".") &&
	      strcmp(dir->d_name,"..")) {
		for (walk = next; walk; walk = walk->next)
		    if (stat_equal(&walk->st,&st.st)) break;
		if (!walk) {
		    this = last_dev(&st,name,major,increment);
		    if (this > max) max = this;
		}
	    }
	}
    }
    (void) closedir(dp);
    return max;
}


#ifndef HDIO_GETGEO
#define HDIO_GETGEO HDIO_REQ
#endif


static void geo_query_dev(GEOMETRY *geo,int device,int all)
{
    DEVICE dev;
    int fd,get_all;
    struct floppy_struct fdprm;
    struct hd_geometry hdprm;

    get_all = all || MAJOR(device) != MAJOR_FD;
    if (get_all) fd = dev_open(&dev,device,O_NOACCESS);
    else fd = -1; /* pacify GCC */
    switch (MAJOR(device)) {
	case MAJOR_FD:
	    geo->device = device & 3;
	    if (!get_all) {
		geo->heads = geo->cylinders = geo->sectors = 1;
		geo->start = 0;
		break;
	    }
	    if (ioctl(fd,FDGETPRM,&fdprm) < 0)
		die("geo_query_dev FDGETPRM (dev 0x%04x): %s",device,
		  strerror(errno));
	    geo->heads = fdprm.head;
	    geo->cylinders = fdprm.track;
	    geo->sectors = fdprm.sect;
	    geo->start = 0;
	    break;
	case MAJOR_HD:
	case MAJOR_XT:
	    geo->device = 0x80+(MINOR(device) >> 6)+(MAJOR(device) == MAJOR_HD ?
	      0 : last_dev(NULL,DEV_DIR,MAJOR_HD,64));
	    if (ioctl(fd,HDIO_GETGEO,&hdprm) < 0)
		die("geo_query_dev HDIO_GETGEO (dev 0x%04x): %s",device,
		  strerror(errno));
	    geo->heads = hdprm.heads;
	    geo->cylinders = hdprm.cylinders;
	    geo->sectors = hdprm.sectors;
	    geo->start = hdprm.start;
	    break;
	case MAJOR_SD:
	    geo->device = 0x80+last_dev(NULL,DEV_DIR,MAJOR_HD,64)+
	      (MINOR(device) >> 4);
	    if (ioctl(fd,HDIO_GETGEO,&hdprm) < 0)
		die("geo_query_dev HDIO_GETGEO (dev 0x%04x): %s",device,
		  strerror(errno));
	    if (all && !hdprm.sectors)
		die("HDIO_REQ not supported for your SCSI controller. Please "
		  "use " DFL_DISKTAB);
	    geo->heads = hdprm.heads;
	    geo->cylinders = hdprm.cylinders;
	    geo->sectors = hdprm.sectors;
	    geo->start = hdprm.start;
	    break;
	default:
	    die("Sorry, don't know how to handle device 0x%04x",device);
    }
    if (get_all) dev_close(&dev);
}


void geo_get(GEOMETRY *geo,int device,int user_device,int all)
{
    DT_ENTRY *walk;

    for (walk = disktab; walk; walk = walk->next)
	if (walk->device == device) break;
    if (!walk || walk->start == -1) geo_query_dev(geo,device,all);
    if (walk) {
	geo->device = walk->bios;
	geo->heads = walk->heads;
	geo->cylinders = walk->cylinders;
	geo->sectors = walk->sectors;
	if (walk->start != -1) geo->start = walk->start;
    }
    if (user_device != -1) geo->device = user_device;
    if (!all) {
	if (verbose > 2)
	printf("Device 0x%04x: BIOS drive 0x%02x, no geometry.\n",device,
	  geo->device);
	return;
    }
    if (!geo->heads || !geo->cylinders || !geo->sectors)
	die("Device 0x%04X: Got bad geometry %d/%d/%d\n",device,
	  geo->sectors,geo->heads,geo->cylinders);
    if (geo->heads > BIOS_MAX_HEADS)
	die("Device 0x%04X: Maximum number of heads is %d, not %d\n",device,
	  BIOS_MAX_HEADS,geo->heads);
    if (geo->sectors > BIOS_MAX_SECS)
	die("Device 0x%04X: Maximum number of sectors is %d, not %d\n",
	  device,BIOS_MAX_SECS,geo->sectors);
    if (verbose > 2) {
	printf("Device 0x%04x: BIOS drive 0x%02x, %d heads, %d cylinders,\n",
	  device,geo->device,geo->heads,geo->cylinders);
	printf("%15s%d sectors. Partition offset: %d sectors.\n","",
	  geo->sectors,geo->start);
    }
}


int geo_open(GEOMETRY *geo,char *name,int flags)
{
    char *here;
    int user_dev,block_size;
    struct stat st;

    if ((here = strrchr(name,':')) == NULL) user_dev = -1;
    else {
	fprintf(stderr,":BIOS syntax is no longer supported. Please use "
	  DFL_DISKTAB "\n");
	*here++ = 0;
	user_dev = to_number(here);
    }
    if ((geo->fd = open(name,flags)) < 0)
	die("open %s: %s",name,strerror(errno));
    if (fstat(geo->fd,&st) < 0) die("fstat %s: %s",name,strerror(errno));
    if (!S_ISREG(st.st_mode) && !S_ISBLK(st.st_mode))
	die("%s: neither a reg. file nor a block dev.",name);
    geo_get(geo,S_ISREG(st.st_mode) ? st.st_dev : st.st_rdev,user_dev,1);
    geo->file = S_ISREG(st.st_mode);
    geo->boot = 0;
#ifndef FIGETBSZ
    geo->spb = 2;
#else
    if (!geo->file) geo->spb = 2;
    else {
	if (ioctl(geo->fd,FIGETBSZ,&block_size) < 0) {
	    fprintf(stderr,"Warning: FIGETBSZ %s: %s\n",name,strerror(errno));
	    geo->spb = 2;
	}
	else {
	    if (!block_size || (block_size & (SECTOR_SIZE-1)))
		die("Incompatible block size: %d\n",block_size);
	    geo->spb = block_size/SECTOR_SIZE;
	}
    }
#endif
    return geo->fd;
}


int geo_open_boot(GEOMETRY *geo,char *name)
{
    struct stat st;

    if (stat(name,&st) < 0) die("stat %s: %s",name,strerror(errno));
    if (!S_ISREG(st.st_mode) && !S_ISBLK(st.st_mode))
	die("%s: neither a reg. file nor a block dev.",name);
    if (MAJOR(st.st_rdev) == MAJOR_FD) geo->fd = 0;
    else if ((geo->fd = open(name,O_NOACCESS)) < 0)
	    die("open %s: %s",name,strerror(errno));
    geo_get(geo,S_ISREG(st.st_mode) ? st.st_dev : st.st_rdev,-1,0);
    geo->file = S_ISREG(st.st_mode);
    geo->boot = 1;
    geo->spb = 1;
    return geo->fd;
}


void geo_close(GEOMETRY *geo)
{
    if (geo->fd) (void) close(geo->fd);
}


#ifndef FIBMAP
#define FIBMAP BMAP_IOCTL
#endif


int geo_comp_addr(GEOMETRY *geo,int offset,SECTOR_ADDR *addr)
{
    int block,sector;

    if (geo->boot && offset >= SECTOR_SIZE)
	die("Internal error: sector > 0 after geo_open_boot");
    block = offset/geo->spb/SECTOR_SIZE;
    if (geo->file) {
	if (ioctl(geo->fd,FIBMAP,&block) < 0) pdie("ioctl FIBMAP");
	if (!block) return 0;
    }
    sector = block*geo->spb+((offset/SECTOR_SIZE) % geo->spb);
    sector += geo->start;
    if (linear) {
	addr->device = geo->device | LINEAR_FLAG;
	addr->sector = sector & 0xff;
	addr->track = (sector >> 8) & 0xff;
	addr->head = sector >> 16;
	if (verbose > 4)
	    printf("fd %d: offset %d -> linear %d\n",geo->fd,offset,sector);
    }
    else {
	addr->device = geo->device;
	addr->sector = (sector % geo->sectors)+1;
	sector /= geo->sectors;
	addr->head = sector % geo->heads;
	sector /= geo->heads;
	if (sector >= BIOS_MAX_CYLS)
	    die("geo_comp_addr: Cylinder number is too big (%d > %d)",sector,
	      BIOS_MAX_CYLS-1);
	if (sector >= geo->cylinders)
	    die("geo_comp_addr: Cylinder %d beyond end of media (%d)",sector,
	      geo->cylinders);
	if (verbose > 4)
	    printf("fd %d: offset %d -> dev %d, head %d, track %d, sector %d\n",
	      geo->fd,offset,addr->device,addr->head,sector,addr->sector);
	addr->track = sector & 255;
	addr->sector |= (sector >> 8) << 6;
    }
    addr->num_sect = 1;
    return 1;
}
