/* File related stuff
   
   Copyright (C) 1996 Maurizio Plaza
   		 1996,1997 Jakub Jelinek
   
   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 <ctype.h>
#include <sys/types.h>
#include <errno.h>
#include "silo.h"
typedef int FILE;
#include <linux/ext2_fs.h>
#include <ext2fs/ext2fs.h>
#define ec ,},{
static struct {
    int errnum;
    char *desc;
} ext2errors[] = { 
{ 0, "OK"
#include <ext2fs/ext2_err.et>
}
};
#include "ufs.h"
#include "romfs.h"
#include <setjmp.h>

static errcode_t linux_open (const char *name, int flags, io_channel * channel);
static errcode_t linux_close (io_channel channel);
static errcode_t linux_set_blksize (io_channel channel, int blksize);
static errcode_t linux_read_blk (io_channel channel, unsigned long block, int count, void *data);
static errcode_t linux_write_blk (io_channel channel, unsigned long block, int count, const void *data);
static errcode_t linux_flush (io_channel channel);

static struct struct_io_manager struct_linux_manager =
{
    EXT2_ET_MAGIC_IO_MANAGER,
    "linux I/O Manager",
    linux_open,
    linux_close,
    linux_set_blksize,
    linux_read_blk,
    linux_write_blk,
    linux_flush
};

ext2_filsys fs = 0;
static ino_t root, cwd;
static io_manager linux_io_manager = &struct_linux_manager;
static do_gunzip = 0;
static unsigned int *gzipped_blocks;
static unsigned int *cur_gzipped_block;

static unsigned int bs;		/* Block Size */
static unsigned long int doff;	/* Block where partition starts */

static unsigned char *filebuffer;
static unsigned char *filelimit;
static int first_block;
static int block_no;
static int block_cnt;
static int last_blockcnt;
static char *gunzip_buffer;
static char *gunzip_inp;
static char *gunzip_endbuf;
static enum { ext2, ufs, romfs } type;

extern int solaris;

static int read_sun_partition (int partno)
{
    int rc;
    sun_partition sdl;
    unsigned short csum, *ush;

    rc = read ((char *) &sdl, 512, 0);
    if (rc != 512) {
	fatal ("Cannot read partition");
	return 0;
    }
    if (sdl.magic != SUN_LABEL_MAGIC)
	fatal ("Wrong disklabel magic");
    for (csum = 0, ush = ((unsigned short *) ((&sdl) + 1)) - 1; ush >= (unsigned short *) &sdl;)
	csum ^= *ush--;
    if (csum)
	printf ("\nWarning: Your disklabel has wrong checksum. Use fdisk to correct it.");
    doff = (sdl.ntrks * sdl.nsect * sdl.partitions[partno - 1].start_cylinder) << 9;
    return 1;
}

int sprintf (char *buf, char *fmt,...)
{
    strcpy (buf, fmt);
}

void com_err (const char *a, long i, const char *fmt,...)
{
    printf ((char *) fmt);
}

static errcode_t linux_open (const char *name, int flags, io_channel * channel)
{
    int partno;
    io_channel io;

    if (!name)
	return EXT2_ET_BAD_DEVICE_NAME;
    io = (io_channel) malloc (sizeof (struct struct_io_channel));
    if (!io)
	return EXT2_ET_BAD_DEVICE_NAME;
    memset (io, 0, sizeof (struct struct_io_channel));
    io->magic = EXT2_ET_MAGIC_IO_CHANNEL;
    io->manager = linux_io_manager;
    io->name = (char *) malloc (strlen (name) + 1);
    strcpy (io->name, name);
    io->block_size = bs;
    io->read_error = 0;
    io->write_error = 0;

    doff = 0;
    if (strncmp (name, "/dev/fd0", 8)) {
        partno = *(name + strlen (name) - 1) - '0';
        if (partno && !read_sun_partition (partno))
	    return EXT2_ET_BAD_DEVICE_NAME;
    }
    *channel = io;
    return 0;
}

static errcode_t linux_close (io_channel channel)
{
}

static errcode_t linux_set_blksize (io_channel channel, int blksize)
{
    channel->block_size = bs = blksize;
}

static errcode_t linux_read_blk (io_channel channel, unsigned long block, int count, void *data)
{
    int size;

    size = (count < 0) ? -count : count * bs;
    if (read (data, size, block * bs + doff) != size) {
	printf ("\nRead error on block %d\n", block);
	return EXT2_ET_SHORT_READ;
    }
    return 0;
}

static errcode_t linux_write_blk (io_channel channel, unsigned long block, int count, const void *data)
{
}

static errcode_t linux_flush (io_channel channel)
{
}

static void ext2fs_error (int errcode)
{
    int i;
    
    for (i = 0; i < sizeof (ext2errors) / sizeof (ext2errors[0]); i++)
    	if (ext2errors [i].errnum == errcode) {
    	    printf ("%s", ext2errors [i].desc);
    	    return;
    	}
    printf ("Unknown ext2 error");
}

static int open_ext2 (char *device)
{
    int retval;

    retval = ext2fs_open (device, EXT2_FLAG_RW, 0, 0, linux_io_manager, &fs);
    if (retval == EXT2_ET_BAD_MAGIC)
        return 0;
    if (retval) {
        printf ("\n");
        ext2fs_error (retval);
        printf ("\n");
	return 0;
    }
    root = EXT2_ROOT_INO;
    return 1;
}

static int open_ufs (char *device)
{
    if (ufs_open (device, linux_io_manager, &fs)) return 0;
    root = UFS_ROOTINO;
    return 1;
}

static int open_romfs (char *device)
{
    if (romfs_open (device, linux_io_manager, &fs)) return 0;
    root = ((struct romfs_super_block *)(fs->io->private_data))->checksum;
    return 1;
}

static void rotate (int freq)
{
    static int i = 0;
    static char rot[] = "\\|/-";

    if (!(i % freq))
	printf ("%c\b", rot[(i / freq) % 4]);
    i++;
}

static int dump_block (ext2_filsys fs, blk_t * blocknr, int blockcnt,
		       void *private)
{
    size_t nbytes;

    if (blockcnt < 0)
	return 0;

    if (!first_block)
	rotate (5);
    if (!first_block && do_gunzip) {
    	if (blockcnt != last_blockcnt + 1) {
            int i;
            
            for (i = blockcnt - last_blockcnt - 1; i > 0; i--)
            	*cur_gzipped_block++ = 0xffffffff;
        }
	*cur_gzipped_block++ = *blocknr;
	last_blockcnt = blockcnt;
	return 0;
    }
    if (*blocknr || block_no) {
	if (first_block || !*blocknr || blockcnt != last_blockcnt + 1 || (block_no && *blocknr != block_no + block_cnt)) {
	    if (first_block) {
		block_no = *blocknr;
		block_cnt = 1;
		if (blockcnt) {
		    fatal ("File cannot have a hole at beginning");
		    return BLOCK_ABORT;
		}
		last_blockcnt = -1;
	    }
	    if (filebuffer + (block_cnt + ((*blocknr) ? (blockcnt - last_blockcnt - 1) : 0)) * bs > filelimit) {
		fatal ("Image too large to fit in destination");
		return BLOCK_ABORT;
	    }
	    if (block_cnt > 0 && io_channel_read_blk (fs->io, block_no, block_cnt, filebuffer))
		return BLOCK_ABORT;
	    if (first_block) {
		first_block = 0;
		last_blockcnt = 0;
		if (*filebuffer == 037 && (filebuffer[1] == 0213 || filebuffer[1] == 0236)) {	/* gzip magic */
		    unsigned long sa = (unsigned long)&_start;
		    gunzip_buffer = malloc (16 * bs);
		    memcpy (gunzip_buffer, filebuffer, bs);
		    gzipped_blocks = (unsigned int *) malloc ((sa / 512) * sizeof (int));
		    cur_gzipped_block = gzipped_blocks;
		    *cur_gzipped_block++ = *blocknr;
		    printf ("Uncompressing image...\n");
		    return 0;
		}
		do_gunzip = 0;
		filebuffer += bs;
		block_no = 0;
		block_cnt = 0;
		return 0;
	    }
	    filebuffer += block_cnt * bs;
	    if (*blocknr && blockcnt && blockcnt != last_blockcnt + 1) {
	    	memset (filebuffer, 0, (blockcnt - last_blockcnt - 1) * bs);
	    	filebuffer += (blockcnt - last_blockcnt - 1) * bs;
	    }
	    block_no = 0;
	}
	if (*blocknr) {
	    if (!block_no) {
		block_no = *blocknr;
		block_cnt = 1;
	    } else
		block_cnt++;
	    last_blockcnt = blockcnt;
	}
    }
    return 0;
}

extern jmp_buf gunzip_env;
static unsigned char get_gzip_input (void)
{

    if (gunzip_inp < gunzip_endbuf)
	return *gunzip_inp++;
    {
	int count = 1;
	unsigned int first = *cur_gzipped_block++;

	if (!first) {
	    printf ("\nDecompression error: run out of compressed data\n");
	    longjmp (gunzip_env, 1);
	}
	if (first == 0xffffffff) {
	    /* Hole */
	    
	    while (*cur_gzipped_block == 0xffffffff && count < 16) {
	    	count++;
	    	cur_gzipped_block++;
	    }
	    rotate (1);
	    memset (gunzip_buffer, 0, count * bs);
	} else {
	    while (*cur_gzipped_block == first + count && count < 16) {
	        count++;
	        cur_gzipped_block++;
	    }
	    rotate (1);
	    if (io_channel_read_blk (fs->io, first, count, gunzip_buffer)) {
	        printf ("\nRead error\n");
	        longjmp (gunzip_env, 1);
	    }
	}
	gunzip_endbuf = gunzip_buffer + count * bs;
	gunzip_inp = gunzip_buffer;
    }
    return *gunzip_inp++;
}

static void unget_gzip_input (void)
{
    if (gunzip_inp > gunzip_buffer)
	gunzip_inp--;
    else {
	printf ("\nInternal error\n");
	longjmp (gunzip_env, 1);
    }
}

static gunzipped_len = 0;

static int dump_finish (void)
{
    if (block_no) {
	blk_t tmp = 0;
	if (dump_block (fs, &tmp, 0, 0))
	    return 0;
    }
    if (do_gunzip) {
	*cur_gzipped_block++ = 0;
	cur_gzipped_block = gzipped_blocks + 1;
	gunzip_endbuf = gunzip_buffer + bs;
	gunzip_inp = gunzip_buffer;
	if ((gunzipped_len = decompress (filebuffer, filelimit, get_gzip_input, unget_gzip_input)) < 0) {
	    free (gzipped_blocks);
	    free (gunzip_buffer);
	    return 0;
	}
    }
    return 1;
}

static int dump_ext2 (ino_t inode, char *filename)
{
    errcode_t retval;

    retval = ext2fs_block_iterate (fs, inode, 0, 0,
				   dump_block, 0);
    if (retval) {
        printf ("\n");
    	ext2fs_error (retval);
        printf ("\n");
	return 0;
    }
    return dump_finish ();
}

static int dump_ufs (ino_t inode, char *filename)
{
    if (ufs_block_iterate (fs, inode, dump_block, 0)) {
	printf ("Error while loading of %s", filename);
	return 0;
    }
    return dump_finish ();
}

static int dump_romfs (ino_t inode, char *filename)
{
    if (romfs_block_iterate (fs, inode, dump_block, 0)) {
	printf ("Error while loading of %s", filename);
	return 0;
    }
    return dump_finish ();
}

int dump_device_range (char *filename, char *bogusdev, int *len, void (*lenfunc)(int, char **, char **))
{
    /* Range of blocks on physical block device */
    int start, end = -1;
    int retval;
    char *p;

    bs = 512;
    p = strchr (filename, '-');
    filename++;
    if (p && *filename >= '0' && *filename <= '9') {
	*p = 0;
	start = atoi (filename);
	filename = p + 1;
	p = strchr (filename, ']');
	if (p && *filename >= '0' && *filename <= '9' && !p[1]) {
	    *p = 0;
	    end = atoi (filename);
	}
    }
    if (end == -1) {
    	if (prom_vers == PROM_V0)
	    printf ("\nRanges of physical blocks are specified as {device_name}{partno}[xx-yy]"
		    "\nwhere {} means optional part and xx is the starting block"
		    "\n(chunk of 512 bytes) and yy end (not inclusive, i.e. yy won't be read)\n");
	else
	    printf ("\nRanges of physical blocks are specified as {prom_path;}{partno}[xx-yy]"
		    "\nwhere {} means optional part, partno defaults to 0 (i.e. whole disk)"
		    "\nand xx is the starting block (chunk of 512 bytes) and yy end (not"
		    "\ninclusive, i.e. yy won't be read)\n");
	return 0;
    }
    if (lenfunc)
        (*lenfunc)((end - start) << 9, (char **)&filebuffer, (char **)&filelimit);
    fs = (ext2_filsys) malloc (sizeof (struct struct_ext2_filsys));
    if (fs) {
	if (!linux_open (bogusdev, 0, &fs->io)) {
	    blk_t tmp;

	    first_block = do_gunzip;
	    block_no = 0;
	    last_blockcnt = 0;
	    block_cnt = 0;
	    for (tmp = start; tmp < end; tmp++) {
		if (dump_block (fs, &tmp, tmp - start, 0))
		    break;
	    }

	    if (tmp == end && dump_finish ()) {
	        if (len) {
	            if (do_gunzip)
	                *len = gunzipped_len;
	            else
	                *len = (end - start) << 9;
	        }
		return 1;
	    }
	}
    }
    printf ("\nInternal error while loading physical blocks from device\n");
    return 0;
}

int dump_cdrom (char *filename, char *bogusdev, int *len, void (*lenfunc)(int, char **, char **))
{
    /* Blocks on cdrom device */
    int start, end = -1;
    int retval;
    int length;
    extern char silo_conf[256];

    bs = 1024;
    bogusdev[8] = '0';
    retval = atoi (filename + 3);
    start = *(unsigned int *)(silo_conf + 8 * retval + 8);
    length = *(unsigned int *)(silo_conf + 8 * retval + 12);
    end = start + ((length + 1023) >> 10);
    if (lenfunc)
        (*lenfunc)(length, (char **)&filebuffer, (char **)&filelimit);
    fs = (ext2_filsys) malloc (sizeof (struct struct_ext2_filsys));
    if (fs) {
	if (!linux_open (bogusdev, 0, &fs->io)) {
	    blk_t tmp;
	    
	    first_block = do_gunzip;
	    block_no = 0;
	    last_blockcnt = 0;
	    block_cnt = 0;
	    for (tmp = start; tmp < end; tmp++) {
		if (dump_block (fs, &tmp, tmp - start, 0))
		    break;
	    }

	    if (tmp == end && dump_finish ()) {
	        if (len) {
	            if (do_gunzip)
	                *len = gunzipped_len;
	            else
	                *len = length;
	        }
		return 1;
	    }
	}
    }
    printf ("\nInternal error while loading physical blocks from device\n");
    return 0;
}

int get_len (ino_t inode)
{
    if (do_gunzip)
	return gunzipped_len;
    if (type == ext2) {
	struct ext2_inode ei;
	int retval;
	    
	if ((retval = ext2fs_read_inode (fs, inode, &ei))) {
            printf ("\n");
	    ext2fs_error (retval);
            printf ("\n");
	    return 0;
	}
	return ei.i_size;
    } else if (type == ufs) {
	struct ufs_inode ui;
	    
	if (ufs_read_inode (fs, inode, &ui))
	    return 0;
	/* Hope nobody is so stupid to load 4GB+ kernel into core :)))) */
	return ufsi_size(&ui);
    } else if (type == romfs) {
    	struct romfs_inode ri;
    	
    	if (romfs_read_inode (fs, inode, &ri))
    	    return 0;
    	if ((ri.next & ROMFH_TYPE) != ROMFH_REG) {
    	    printf("romfs: get_len on non-reg file?\n");
    	    return 0;
    	}
    	return ri.size;
    }
    return 0;
}

int load_file (char *device, int partno, char *filename, char *buffer, char *limit, int *len, int dogunzip, void (*lenfunc)(int, char **, char **))
{
    ino_t inode;
    int retval;
    int size = -1;
    char bogusdev[] = "/dev/sdaX";
    char *bogdev;

    if (!device)
	device = bootdevice;
#if 0
    printf ("Loading %s from %d\n", filename, partno);
#endif
    bogdev = bogusdev;
    if (prom_vers == PROM_V0) {
    	if (device[0] == 'f' && device[1] == 'd') {
    	    device = "fd()";
    	    bogdev = "/dev/fd0";
    	} else
            bogusdev[8] = partno + '0';
    } else
        bogusdev[8] = partno + '0';
    if (setdisk (device) < 0)
	return 0;
    do_gunzip = dogunzip;
    filebuffer = buffer;
    filelimit = limit;
    if (!strncmp (filename, "!cd", 3)) {
    	solaris = 0;
        return dump_cdrom (filename, bogdev, len, lenfunc);
    }
    if (*filename == '[') {
    	solaris = 0;
	return dump_device_range (filename, bogdev, len, lenfunc);
    }
    if (!open_ext2 (bogdev)) {
        if (!open_romfs (bogdev)) {
            if (!open_ufs (bogdev)) {
                fatal ("Unable to open filesystem");
                return 0;
            } else type = ufs;
        } else {
            type = romfs;
            solaris = 0;
        }
    } else {
        type = ext2;
        solaris = 0;
    }
    if (type == ext2) {
        if (retval = ext2fs_namei_follow (fs, root, root, filename, &inode)) {
	    printf ("\nCannot find %s (", filename);
	    ext2fs_error (retval);
	    printf (")\n");
	    ext2fs_close (fs);
	    return 0;
        }
    } else if (type == romfs) {
        if (retval = romfs_namei (fs, root, root, filename, &inode)) {
	    printf ("\nCannot find %s\n", filename);
	    return 0;
        }
    } else if (type == ufs) {
    	cwd = root;
    	if (solaris) {
    	    if (!ufs_namei (fs, root, root, "/platform", &cwd)) {
    	    	if (!ufs_namei (fs, root, cwd, get_syspackage(), &inode))
    	    	    cwd = inode;
    	    	else {
    	    	    char *p = "sun4c";
    	    	    
    	    	    switch (get_architecture()) {
    	    	    	case sun4: p = "sun4"; break;
    	    	    	case sun4c: p = "sun4c"; break;
    	    	    	case sun4m: p = "sun4m"; break;
    	    	    	case sun4d: p = "sun4d"; break;
    	    	    	case sun4e: p = "sun4e"; break;
    	    	    	case sun4u: p = "sun4u"; break;
    	    	    }
    	    	    if (!ufs_namei (fs, root, cwd, p, &inode))
    	    	        cwd = inode;
    	    	}
    	    }
    	    if (cwd != root && *filename == '/') filename++;
    	}
        if (ufs_namei (fs, root, cwd, filename, &inode)) {
	    printf ("\nCannot find %s.", filename);
	    ufs_close (fs);
	    return 0;
        }
        if (solaris) {
            ino_t sinode;
            
            if (ufs_namei (fs, root, cwd, "ufsboot", &sinode)) {
                printf ("\nCannot find Solaris kernel bootloader `ufsboot'. Will try to load it,\n"
                        "but it may fail\n");
                solaris = 0;
            } else
            	inode = sinode;
        }
    }
    if (lenfunc) {
        do_gunzip = 0;
        (*lenfunc)(size = get_len (inode), (char **)&filebuffer, (char **)&filelimit);
        do_gunzip = dogunzip;
    }
    first_block = do_gunzip;
    last_blockcnt = 0;
    block_no = 0;
    block_cnt = 0;
    retval = 0;
    if (inode)
        switch (type) {
            case ext2: retval = dump_ext2 (inode, filename); break;
            case ufs: retval = dump_ufs (inode, filename); break;
            case romfs: retval = dump_romfs (inode, filename); break;
        }
    if (retval && len)
        *len = (size != -1) ? size : get_len (inode);
    switch (type) {
	case ext2: ext2fs_close (fs); break;
	case ufs: ufs_close (fs); break;
	case romfs: romfs_close (fs); break;
    }
    return retval;
}
