/**
 * @file   ole.c
 * @author Alex Ott, Victor B Wagner
 * @date   Wed Jun 11 12:33:01 2003
 * Version: $Id: ole.c,v 1.2 2006/02/25 15:28:14 vitus Exp $
 * Copyright: Victor B Wagner, 1996-2003 Alex Ott, 2003
 *
 * @brief  Parsing structure of MS Office compound document
 *
 * This file is part of catdoc project
 * and distributed under GNU Public License
 *
 */
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#include "catdoc.h"

#define min(a,b) ((a) < (b) ? (a) : (b))

const static unsigned char ole_sign[]={0xD0,0xCF,0x11,0xE0,0xA1,0xB1,0x1A,0xE1,0};


/**
 * Initializes ole structure
 *
 * @param f (FILE *) compound document file, positioned at bufSize
 *           byte. Might be pipe or socket
 * @param buffer (void *) bytes already read from f
 * @param bufSize number of bytes already read from f should be less
 *                than 512
 *
 * @return
 */
FILE* ole_init(FILE *f, void *buffer, size_t bufSize, struct ole_params_t *ole_params)  {
    unsigned char oleBuf[BBD_BLOCK_SIZE];
    unsigned char *tmpBuf;
    FILE *newfile;
    int ret=0, i;
    long int sbdMaxLen, sbdCurrent, propMaxLen, propCurrent, mblock, msat_size;
    oleEntry *tEntry;
    long int sectorSize;
    long int shortSectorSize;
    long int bbdNumBlocks;

    /* deleting old data (if it was allocated) */
    ole_finish(ole_params);

    if (fseek(f,0,SEEK_SET) == -1) {
        if (errno == ESPIPE) {
            /* We got non-seekable file, create temp file */
            if ((newfile=tmpfile()) == NULL) {
                return NULL;
            }
            if (bufSize > 0) {
                ret=fwrite(buffer, 1, bufSize, newfile);
                if (ret != bufSize) {
                    return NULL;
                }
            }

            while (!feof(f)) {
                ret=fread(oleBuf,1,BBD_BLOCK_SIZE,f);
                fwrite(oleBuf, 1, ret, newfile);
            }
            fseek(newfile,0,SEEK_SET);
        } else {
            return NULL;
        }
    } else {
        newfile=f;
    }
    fseek(newfile,0,SEEK_END);
    ole_params->fileLength=ftell(newfile);

    fseek(newfile,0,SEEK_SET);
    ret=fread(oleBuf,1,BBD_BLOCK_SIZE,newfile);
    if (ret != BBD_BLOCK_SIZE) {
        return NULL;
    }
    if (strncmp(oleBuf,ole_sign,8) != 0) {
        return NULL;
    }
    ole_params->sectorSize = 1<<getshort(oleBuf,0x1e);
    if (ole_params->sectorSize == 0) {
        return NULL;
    }
    sectorSize = ole_params->sectorSize;
    ole_params->shortSectorSize = 1<<getshort(oleBuf,0x20);
    shortSectorSize= ole_params->shortSectorSize;
    if (shortSectorSize > sectorSize) {
        return NULL;
    }
/* Read BBD into memory */
    ole_params->bbdNumBlocks = getulong(oleBuf,0x2c);
    bbdNumBlocks = ole_params->bbdNumBlocks;
    if ((ole_params->BBD=malloc(bbdNumBlocks*sectorSize)) == NULL) {
        return NULL;
    }

    if ((tmpBuf=malloc(MSAT_ORIG_SIZE)) == NULL) {
        return NULL;
    }
    memcpy(tmpBuf,oleBuf+0x4c,MSAT_ORIG_SIZE);
    mblock=getlong(oleBuf,0x44);
    msat_size=getlong(oleBuf,0x48);

/*  fprintf(stderr, "msat_size=%ld\n", msat_size); */

    i=0;
    while ((mblock >= 0) && (i < msat_size)) {
        unsigned char *newbuf;
/*      fprintf(stderr, "i=%d mblock=%ld\n", i, mblock); */
        if ((newbuf=realloc(tmpBuf, sectorSize*(i+1)+MSAT_ORIG_SIZE)) != NULL) {
            tmpBuf=newbuf;
        } else {
            free(tmpBuf);
            ole_finish(ole_params);
            return NULL;
        }

        fseek(newfile, 512+mblock*sectorSize, SEEK_SET);
        if (fread(tmpBuf+MSAT_ORIG_SIZE+(sectorSize-4)*i,
                  1, sectorSize, newfile) != sectorSize) {
            ole_finish(ole_params);
            return NULL;
        }

        i++;
        mblock=getlong(tmpBuf, MSAT_ORIG_SIZE+(sectorSize-4)*i);
    }

/*  fprintf(stderr, "bbdNumBlocks=%ld\n", bbdNumBlocks); */
    for (i=0; i< bbdNumBlocks; i++) {
        long int bbdSector=getlong(tmpBuf,4*i);

        if (bbdSector >= ole_params->fileLength/sectorSize || bbdSector < 0) {
            errno = EINVAL;
            ole_finish(ole_params);
            return NULL;
        }
        fseek(newfile, 512+bbdSector*sectorSize, SEEK_SET);
        if (fread(ole_params->BBD+i*sectorSize, 1, sectorSize, newfile) != sectorSize) {
            free(tmpBuf);
            ole_finish(ole_params);
            return NULL;
        }
    }
    free(tmpBuf);

/* Read SBD into memory */
    ole_params->sbdLen=0;
    sbdMaxLen=10;
    sbdCurrent = ole_params->sbdStart = getlong(oleBuf,0x3c);
    if (ole_params->sbdStart > 0) {
        if ((ole_params->SBD=malloc(sectorSize*sbdMaxLen)) == NULL) {
            ole_finish(ole_params);
            return NULL;
        }
        while (1) {
            fseek(newfile, 512+sbdCurrent*sectorSize, SEEK_SET);
            fread(ole_params->SBD+ole_params->sbdLen*sectorSize, 1, sectorSize, newfile);
            ole_params->sbdLen++;
            if (ole_params->sbdLen >= sbdMaxLen) {
                unsigned char *newSBD;

                sbdMaxLen+=5;
                if ((newSBD=realloc(ole_params->SBD, sectorSize*sbdMaxLen)) != NULL) {
                    ole_params->SBD=newSBD;
                } else {
                    ole_finish(ole_params);
                    return NULL;
                }
            }
            if (sbdCurrent < 0 || sbdCurrent * 4 >= bbdNumBlocks * sectorSize)
            {
                break;
            }
            sbdCurrent = getlong(ole_params->BBD, sbdCurrent*4);
            if (sbdCurrent < 0 ||
                sbdCurrent >= ole_params->fileLength/sectorSize)
                break;
        }
        ole_params->sbdNumber = (ole_params->sbdLen*sectorSize)/shortSectorSize;
    } else {
        ole_params->SBD=NULL;
    }
/* Read property catalog into memory */
    ole_params->propLen = 0;
    propMaxLen = 5;
    propCurrent = ole_params->propStart = getlong(oleBuf,0x30);
    if (ole_params->propStart >= 0) {
        if ((ole_params->properties=malloc(propMaxLen*sectorSize)) == NULL) {
            ole_finish(ole_params);
            return NULL;
        }
        while (1) {
/*              fprintf(stderr, "propCurrent=%ld\n",propCurrent); */
            fseek(newfile, 512+propCurrent*sectorSize, SEEK_SET);
            fread(ole_params->properties+ole_params->propLen*sectorSize,
                  1, sectorSize, newfile);
            (ole_params->propLen)++;
            if (ole_params->propLen >= propMaxLen) {
                unsigned char *newProp;

                propMaxLen+=5;
                if ((newProp=realloc(ole_params->properties, propMaxLen*sectorSize)) != NULL)
                    ole_params->properties=newProp;
                else {
                    ole_finish(ole_params);
                    return NULL;
                }
            }

            propCurrent = getlong(ole_params->BBD, propCurrent*4);
            if (propCurrent < 0 ||
                propCurrent >= ole_params->fileLength/sectorSize) {
                break;
            }
        }

        ole_params->propNumber = (ole_params->propLen*sectorSize)/PROP_BLOCK_SIZE;
        ole_params->propCurNumber = 0;
    } else {
        ole_finish(ole_params);
        ole_params->properties = NULL;
        return NULL;
    }


/* Find Root Entry */
    while ((tEntry=(oleEntry*)ole_readdir(newfile, ole_params)) != NULL) {
        if (tEntry->type == oleRootDir) {
            ole_params->rootEntry=tEntry;
            break;
        }
        ole_close((FILE*)tEntry);
    }
    ole_params->propCurNumber = 0;
    fseek(newfile, 0, SEEK_SET);
    if (!ole_params->rootEntry) {
        errno = EINVAL;
        ole_finish(ole_params);
        return NULL;
    }
    return newfile;
}

/**
 *
 *
 * @param oleBuf
 *
 * @return
 */
int rightOleType(unsigned char *oleBuf) {
    return (oleBuf[0x42] == 1 || oleBuf[0x42] == 2 ||
            oleBuf[0x42] == 3 || oleBuf[0x42] == 5);
}

/**
 *
 *
 * @param oleBuf
 *
 * @return
 */
oleType getOleType(unsigned char *oleBuf) {
    return (oleType)((unsigned char)oleBuf[0x42]);
}

/**
 * Reads next directory entry from file
 *
 * @param name buffer for name converted to us-ascii should be at least 33 chars long
 * @param size size of file
 *
 * @return 0 if everything is ok -1 on error
 */
FILE *ole_readdir(FILE *f, struct ole_params_t *ole_params) {
    int i, nLen;
    unsigned char *oleBuf;
    oleEntry *e=NULL;
    long int chainMaxLen, chainCurrent;

    if (ole_params->properties == NULL || ole_params->propCurNumber >= ole_params->propNumber || f == NULL)
        return NULL;
    oleBuf=ole_params->properties + ole_params->propCurNumber*PROP_BLOCK_SIZE;
    if (!rightOleType(oleBuf))
        return NULL;
    if ((e = (oleEntry*)malloc(sizeof(oleEntry))) == NULL) {
        return NULL;
    }
    e->dirPos=oleBuf;
    e->type=getOleType(oleBuf);
    e->file=f;
    e->startBlock=getlong(oleBuf,0x74);
    e->blocks=NULL;

    nLen=getshort(oleBuf,0x40);
    for (i=0; i < nLen/2 && i < OLENAMELENGHT; i++)
        e->name[i]=(char)oleBuf[i*2];
    e->name[i]='\0';
    (ole_params->propCurNumber)++;
    e->length=getulong(oleBuf,0x78);
/* Read sector chain for object */
    chainMaxLen = 25;
    e->numOfBlocks = 0;
    chainCurrent = e->startBlock;
    e->isBigBlock = (e->length >= 0x1000) || !strcmp(e->name, "Root Entry");
/*  fprintf(stderr, "e->name=%s e->length=%ld\n", e->name, e->length); */
/*  fprintf(stderr, "e->startBlock=%ld BBD=%p\n", e->startBlock, BBD); */
    if (e->startBlock >= 0 &&
        e->length >= 0 &&
        (e->startBlock <=
         ole_params->fileLength/(e->isBigBlock ? ole_params->sectorSize : ole_params->shortSectorSize))) {
        if ((e->blocks=malloc(chainMaxLen*sizeof(long int))) == NULL) {
            return NULL;
        }
        while (1) {
/*          fprintf(stderr, "chainCurrent=%ld\n", chainCurrent); */
            e->blocks[e->numOfBlocks++] = chainCurrent;
            if (e->numOfBlocks >= chainMaxLen) {
                long int *newChain;
                chainMaxLen+=25;
                if ((newChain=realloc(e->blocks,
                                      chainMaxLen*sizeof(long int))) != NULL)
                    e->blocks=newChain;
                else {
                    free(e->blocks);
                    e->blocks=NULL;
                    return NULL;
                }
            }
            if (e->isBigBlock) {
                chainCurrent = getlong(ole_params->BBD, chainCurrent*4);
            } else if (ole_params->SBD != NULL) {
                chainCurrent = getlong(ole_params->SBD, chainCurrent*4);
            } else {
                chainCurrent=-1;
            }
            if (chainCurrent <= 0 ||
                chainCurrent >= (e->isBigBlock ?
                                 ((ole_params->bbdNumBlocks*ole_params->sectorSize)/4)
                                 : ((ole_params->sbdNumber*ole_params->shortSectorSize)/4)) ||
                (e->numOfBlocks >
                 e->length/(e->isBigBlock ? ole_params->sectorSize : ole_params->shortSectorSize))) {
/*                  fprintf(stderr, "chain End=%ld\n", chainCurrent);   */
                break;
            }
        }
    }

    if (e->length > (e->isBigBlock ? ole_params->sectorSize : ole_params->shortSectorSize)*e->numOfBlocks)
        e->length = (e->isBigBlock ? ole_params->sectorSize : ole_params->shortSectorSize)*e->numOfBlocks;
/*  fprintf(stderr, "READDIR: e->name=%s e->numOfBlocks=%ld length=%ld\n", */
/*                  e->name, e->numOfBlocks, e->length); */

    return (FILE*)e;
}

/**
 * Open stream, which correspond to directory entry last read by
 * ole_readdir
 *
 *
 * @return opaque pointer to pass to ole_read, casted to (FILE *)
 */
int ole_open(FILE *stream) {
    oleEntry *e=(oleEntry *)stream;
    if (e->type != oleStream)
        return -2;

    e->ole_offset=0;
    e->file_offset= ftell(e->file);
    return 0;
}

/**
 *
 *
 * @param e
 * @param blk
 *
 * @return
 */
long int calcFileBlockOffset(oleEntry *e, long int blk, struct ole_params_t *ole_params) {
    long int res;
    if (e->isBigBlock) {
        res=512+e->blocks[blk]*ole_params->sectorSize;
    } else {
        long int sbdPerSector=(ole_params->sectorSize)/(ole_params->shortSectorSize);
        long int sbdSecNum=e->blocks[blk]/sbdPerSector;
        long int sbdSecMod=e->blocks[blk]%sbdPerSector;

        res=512 + ole_params->rootEntry->blocks[sbdSecNum]*ole_params->sectorSize + sbdSecMod*ole_params->shortSectorSize;
    }
    return res;
}


/**
 * Reads block from open ole stream interface-compatible with fread
 *
 * @param ptr pointer to buffer for read to
 * @param size size of block
 * @param nmemb size in blocks
 * @param stream pointer to FILE* structure
 *
 * @return number of readed blocks
 */
size_t ole_read(void *ptr, size_t size, size_t nmemb, FILE *stream, struct ole_params_t *ole_params) {
    oleEntry *e = (oleEntry*)stream;
    long int llen = size*nmemb, rread=0, i;
    long int blockNumber, modBlock, toReadBlocks, toReadBytes, bytesInBlock;
    long int ssize;             /**< Size of block */
    long int newoffset;
    unsigned char *cptr = ptr;
    if (e->ole_offset+llen > e->length)
        llen= e->length - e->ole_offset;

    ssize = (e->isBigBlock ? ole_params->sectorSize : ole_params->shortSectorSize);
    blockNumber=e->ole_offset/ssize;
/*  fprintf(stderr, "blockNumber=%ld e->numOfBlocks=%ld llen=%ld\n", */
/*          blockNumber, e->numOfBlocks, llen); */
    if (blockNumber >= e->numOfBlocks || llen <=0)
        return 0;

    modBlock=e->ole_offset%ssize;
    bytesInBlock = ssize - modBlock;
    if (bytesInBlock < llen) {
        toReadBlocks = (llen-bytesInBlock)/ssize;
        toReadBytes = (llen-bytesInBlock)%ssize;
    } else {
        toReadBlocks = toReadBytes = 0;
    }
/*  fprintf(stderr, "llen=%ld toReadBlocks=%ld toReadBytes=%ld bytesInBlock=%ld blockNumber=%ld modBlock=%ld\n", */
/*          llen, toReadBlocks, toReadBytes, bytesInBlock, blockNumber, modBlock); */
    newoffset = calcFileBlockOffset(e,blockNumber, ole_params)+modBlock;
    if (e->file_offset != newoffset) {
        fseek(e->file, e->file_offset=newoffset, SEEK_SET);
    }
    rread=fread(ptr, 1, min(llen,bytesInBlock), e->file);
    e->file_offset += rread;
    for (i=0; i<toReadBlocks; i++) {
        int readbytes;
        blockNumber++;
        newoffset = calcFileBlockOffset(e,blockNumber, ole_params);
        if (newoffset != e->file_offset);
        fseek(e->file, e->file_offset=newoffset, SEEK_SET);
        readbytes=fread(cptr+rread, 1, min(llen-rread, ssize), e->file);
        rread +=readbytes;
        e->file_offset +=readbytes;
    }
    if (toReadBytes > 0) {
        int readbytes;
        blockNumber++;
        newoffset = calcFileBlockOffset(e,blockNumber, ole_params);
        fseek(e->file, e->file_offset=newoffset, SEEK_SET);
        readbytes=fread(cptr+rread, 1, toReadBytes,e->file);
        rread +=readbytes;
        e->file_offset +=readbytes;
    }
/*  fprintf(stderr, "ole_offset=%ld rread=%ld llen=%ld\n",
    e->ole_offset, rread, llen);*/
    e->ole_offset+=rread;
    return rread;
}

/**
 *
 *
 * @param stream
 *
 * @return
 */
int ole_eof(FILE *stream) {
    oleEntry *e=(oleEntry*)stream;
/*  fprintf(stderr, "EOF: e->ole_offset=%ld  e->length=%ld\n",
    e->ole_offset,  e->length);*/
    return (e->ole_offset >=  e->length);
}

/**
 *
 *
 */
void ole_finish(struct ole_params_t *ole_params) {
    if (ole_params->BBD != NULL) free(ole_params->BBD);
    if (ole_params->SBD != NULL) free(ole_params->SBD);
    if (ole_params->properties != NULL) free(ole_params->properties);
    if (ole_params->rootEntry != NULL) ole_close((FILE*)(ole_params->rootEntry));
    ole_params->properties = ole_params->SBD = ole_params->BBD = NULL;
    ole_params->rootEntry = NULL;
}

/**
 *
 *
 * @param stream
 *
 * @return
 */
int ole_close(FILE *stream) {
    oleEntry *e=(oleEntry*)stream;
    if (e == NULL)
        return -1;
    if (e->blocks != NULL)
        free(e->blocks);
    free(e);
    return 0;
}

/**
 *
 *
 * @param stream pointer to OLE stream structure
 * @param offset
 * @param whence
 *
 * @return
 */
int ole_seek(FILE *stream, long offset, int whence, struct ole_params_t *ole_params) {
    oleEntry *e=(oleEntry*)stream;
    long int new_ole_offset=0, new_file_offset;
    int ssize, modBlock, blockNumber;

    switch (whence) {
    case SEEK_SET:
        new_ole_offset=offset;
        break;

    case SEEK_CUR:
        new_ole_offset=e->ole_offset+offset;
        break;

    case SEEK_END:
        new_ole_offset=e->length+offset;
        break;

    default:
        errno=EINVAL;
        return -1;
    }
    if (new_ole_offset<0)
        new_ole_offset=0;
    if (new_ole_offset >= e->length)
        new_ole_offset=e->length;

    ssize = (e->isBigBlock ? ole_params->sectorSize : ole_params->shortSectorSize);
    blockNumber=new_ole_offset/ssize;
    if (blockNumber >= e->numOfBlocks)
        return -1;

    modBlock=new_ole_offset%ssize;
    new_file_offset = calcFileBlockOffset(e,blockNumber, ole_params)+modBlock;
    fseek(e->file, e->file_offset=new_file_offset, SEEK_SET);
    e->ole_offset=new_ole_offset;

    return 0;
}

/**
 * Tell position inside OLE stream
 *
 * @param stream pointer to OLE stream
 *
 * @return current position inside OLE stream
 */
long ole_tell(FILE *stream) {
    oleEntry *e=(oleEntry*)stream;
    return e->ole_offset;
}


void set_ole_func(struct io_funcs_t *io_funcs) {
    io_funcs->catdoc_read=ole_read;
    io_funcs->catdoc_eof=ole_eof;
    io_funcs->catdoc_seek=ole_seek;
    io_funcs->catdoc_tell=ole_tell;
}


size_t my_fread(void *ptr, size_t size, size_t nmemb, FILE *stream, struct ole_params_t *ole_params)
{
    return fread(ptr, size, nmemb, stream);
}

int my_fseek(FILE *stream, long offset, int whence, struct ole_params_t *ole_params)
{
    return fseek(stream, offset, whence);
}

void set_std_func(struct io_funcs_t *io_funcs) {
    io_funcs->catdoc_read=my_fread;
    io_funcs->catdoc_eof=feof;
    io_funcs->catdoc_seek=my_fseek;
    io_funcs->catdoc_tell=ftell;
}
