/*
 * Copyright (c) 2002, The EROS Group, LLC and Johns Hopkins
 * University. All rights reserved.
 * 
 * This software was developed to support the EROS secure operating
 * system project (http://www.eros-os.org). The latest version of
 * the OpenCM software can be found at http://www.opencm.org.
 * 
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 
 * 2. Redistributions in binary form must reproduce the above
 *    copyright notice, this list of conditions and the following
 *    disclaimer in the documentation and/or other materials
 *    provided with the distribution.
 * 
 * 3. Neither the name of the The EROS Group, LLC nor the name of
 *    Johns Hopkins University, nor the names of its contributors
 *    may be used to endorse or promote products derived from this
 *    software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <opencm.h>
#include "../../zlib/zlib.h"

#define BUFFER_BLOCK_SIZE 1024
#define ZLIB_BUFFER_SIZE 1024

struct Buffer {
  Serializable ser;

  OC_bool frozen;

  ocmoff_t CMVAR(bias);		/* for window buffers */
  ocmoff_t CMVAR(end);

  struct ObVec *CMVAR(vec);
};


#define BLKNDX(pos) ((pos) / BUFFER_BLOCK_SIZE)
#define BLKOFF(pos) ((pos) % BUFFER_BLOCK_SIZE)

OC_bool
buffer_check(const void *v)
{
  return TRUE;
}

void
buffer_show(const void *vp)
{
  const Buffer *buf = vp;
  
  report(0, "Frozen:   %c\n", buf->frozen ? 'Y' : 'N');
  report(0, "Bias:     %s\n", xunsigned64_str(CMGET(buf,bias)));
  report(0, "End:      %s\n", xunsigned64_str(CMGET(buf,end)));
  report(0, "Descrip: <omitted>\n");
}

/* This is mildly tricky. When we serialize a buffer OUT, we must
   serialize it in such a way that it reads back in as an unwindowed
   buffer. This means that the /end/ value that is serialized out must
   be reduced by the /bias/ field. */
void
buffer_serialize(SDR_stream *strm, const void *vp)
{
  const Buffer *buf = vp;

  sdr_w_u64("len", strm, CMGET(buf,end) - CMGET(buf,bias));
  sdr_w_buffer("bits", strm, (Buffer *)vp);
}

void *
buffer_deserialize(const DeserializeInfo *di, SDR_stream *strm)
{
  ocmoff_t len = sdr_r_u64("len", strm);

  return sdr_r_buffer("bits", strm, len);
}

void
buffer_mark(Repository *r, const void *container,
	    const void *ob, rbtree *memObs)
{
  /* do nothing */
}

Buffer *
buffer_create(void)
{
  Buffer *buf = (Buffer *) GC_MALLOC(sizeof(Buffer));
  ser_init(buf, &Buffer_SerType, Buffer_SerType.ver);
  SER_MODIFIED(buf);

  buf->frozen = FALSE;
  CMSET(buf,bias,0);
  CMSET(buf,end,0);
  CMSET(buf,vec,obvec_create());

  CM_CANFREEZE(buf);
  return buf;
}

Buffer *
buffer_fromBuffer(const Buffer *in, ocmoff_t start, ocmoff_t len)
{
  Buffer *buf = buffer_create();

  if (!in->frozen)
    THROW(ExBadValue, "buffer_fromBuffer(): Input buffer is not frozen");

  buf->frozen = TRUE;
  CMSET(buf,bias,start + CMGET(in,bias));
  CMSET(buf,end,CMGET(buf,bias) + len);
  CMSET(buf,vec,CMGET(in,vec));

  if (CMGET(buf,end) > CMGET(in,end))
    THROW(ExOverrun, "buffer_fromBuffer(): new buffer would overrun old");

  CM_CANFREEZE(buf);
  return buf;
}

void
buffer_freeze(Buffer *buf)
{
  buf->frozen = TRUE;
}

ocmoff_t
buffer_length(const Buffer *buf)
{
  return CMGET(buf,end) - CMGET(buf,bias);
}

/* buffer_append() can only be applied on non-frozen buffers. Mutable
   buffers always have a bias value of zero. */
void
buffer_append(Buffer *buf, const void *vp, ocmoff_t len)
{
  ocmoff_t end = CMGET(buf,end) + len;
  unsigned char *bp = (unsigned char *) vp;

  while (BLKNDX(end) >= vec_size(CMGET(buf,vec))) {
    unsigned char* block = GC_MALLOC_ATOMIC(BUFFER_BLOCK_SIZE);
    obvec_append(CMCLOBBER(buf,vec), block);
  }
  
  while (len > 0) {
    ocmoff_t start = CMGET(buf,end);
    size_t blkoff = BLKOFF(start);
    size_t take = min(BUFFER_BLOCK_SIZE - blkoff, len);
    unsigned char *block = 
      vec_fetch(CMGET(buf,vec), BLKNDX(start), unsigned char *);

    block += blkoff;
    memcpy(block, bp, take);
    bp += take;
    CMSET(buf,end, CMGET(buf,end) + take);
    len -= take;
  }
}

void
buffer_appendString(Buffer *buf, const char *s)
{
  buffer_append(buf, s, strlen(s));
}

/* getChunk returns the next linear byte subsequence in a Buffer,
   along with the length of that sequence. It honors both the length
   restrictions on the buffer and also any windowing that has been
   applied (the bias field). */
BufferChunk
buffer_getChunk(const Buffer *buf, ocmoff_t pos, ocmoff_t len)
{
  pos += CMGET(buf,bias);

  if (pos >= CMGET(buf,end))
    THROW(ExOverrun, "Buffer length exceeded in buffer_getChunk\n");

  {
    BufferChunk bc;
    unsigned blkoff = BLKOFF(pos);
    ocmoff_t take = min(BUFFER_BLOCK_SIZE - blkoff, CMGET(buf,end) - pos);
    unsigned char *block = 
      vec_fetch(CMGET(buf,vec), BLKNDX(pos), unsigned char *);

    bc.ptr = block + blkoff;
    bc.len = min(take, len);

    return bc;
  }
}

Buffer *
buffer_FromFilePtr(const char *name, FILE *f)
{
  Buffer *buf = buffer_create();

  rewind(f);

  for ( ;; ) {
    unsigned char *block = GC_MALLOC_ATOMIC(BUFFER_BLOCK_SIZE);

    size_t take = fread(block, 1, BUFFER_BLOCK_SIZE, f);
    if (take == 0 && feof(f))
      break;
    else if (take == 0)
      THROW(ExTruncated, format("Could not read all of \"%s\" (errno %d)",
				name, errno));

    obvec_append(CMCLOBBER(buf,vec), block);
    CMSET(buf,end,CMGET(buf,end) + take);
  }

  buf->frozen = TRUE;

  CM_CANFREEZE(buf);
  return buf;
}

Buffer *
buffer_FromFile(const char *fileName, unsigned char eType)
{
  FILE *f = NULL;
  Buffer *buf = NULL;

  assert(eType == 'T' || eType == 'B');
  
  TRY {
    f = xfopen(fileName, 'r', (eType == 'T') ? 't' : 'b');
  
    buf = buffer_FromFilePtr(fileName, f);

    xfclose(f);
  }
  DEFAULT(ex) {
    xfclose(f);
    RETHROW(ex);
  }
  END_CATCH;

  CM_CANFREEZE(buf);
  return buf;
}

void 
buffer_ToFile(const Buffer *buf, const char *fileName, 
	      unsigned char eType)
{
  FILE *f = NULL;
  const char *dir;

  assert(eType == 'T' || eType == 'B');
  
  dir = path_dirname(fileName);
  path_smkdir(dir);
			
  TRY {
    ocmoff_t end = buffer_length(buf);
    ocmoff_t pos = 0;

    f = xfopen(fileName, 'w', (eType == 'T') ? 't' : 'b');

    while (pos < end) {
      BufferChunk bc = buffer_getChunk(buf, pos, end - pos);

      if (fwrite(bc.ptr, 1, bc.len, f) < bc.len)
	THROW(ExTruncated, format("Could not write all of \"%s\"", fileName));

      pos += bc.len;
    }

    xfclose(f);
  }
  DEFAULT(ex) {
    xfclose(f);
    RETHROW(ex);
  }
  END_CATCH;

}

int
buffer_getc(const Buffer *buf, ocmoff_t pos)
{
  pos += CMGET(buf,bias);

  if (pos >= CMGET(buf,end))
    THROW(ExOverrun, "Buffer size exceeded");


  {
    unsigned char *block = 
      vec_fetch(CMGET(buf,vec), BLKNDX(pos), unsigned char *);
    size_t blkoff = BLKOFF(pos);

    return block[blkoff];
  }
}

void 
buffer_read(const Buffer *buf, void *vp, ocmoff_t pos, ocmoff_t len)
{
  ocmoff_t end = pos + len;
  unsigned char *bp = vp;

  while (pos < end) {
    BufferChunk bc = buffer_getChunk(buf, pos, end - pos);
    assert(bc.len <= (end - pos));

    memcpy(bp, bc.ptr, bc.len);

    bp += bc.len;
    pos += bc.len;
  }
}

/* Note that buffer_asString ALWAYS NUL-terminates whatever it
   returns. If you wanted a binary bytestring, the extra NUL won't
   really hurt you. If you needed a NUL, though, it's absence could
   prove quite irritating. */
char *
buffer_asString(const Buffer *buf)
{
  ocmoff_t len = buffer_length(buf);
  char *s = GC_MALLOC_ATOMIC(len+1);
  s[len] = 0;

  buffer_read(buf, s, 0, len);

  return s;
}

Buffer *
buffer_compress(Buffer *inBuf, int level)
{
  char buffer[ZLIB_BUFFER_SIZE];
  z_stream zstrm;
  ocmoff_t pos = 0;
  ocmoff_t end = buffer_length(inBuf);
  Buffer *outBuf = buffer_create();
  int result = Z_OK;

  memset(&zstrm, 0, sizeof(zstrm));

  zstrm.zalloc = Z_NULL;
  zstrm.zfree = Z_NULL;
  zstrm.opaque = Z_NULL;
  zstrm.total_out= 0;
  zstrm.total_in= 0;
  zstrm.data_type = Z_BINARY;

  zstrm.next_out = buffer;
  zstrm.avail_out = ZLIB_BUFFER_SIZE;

  zstrm.avail_in = 0;

  log_trace(DBG_COMPRESS, format("Compressing %d %d\n", level, (int)buffer_length(inBuf)));

  if(deflateInit(&zstrm, level) != Z_OK)
    THROW(ExBadValue, "Could not initialize zlib: deflateInit");

  while(result != Z_STREAM_END)
  {
    /* add more if the zlib buffer is empty and there is more in inBuf */
    if (zstrm.avail_in == 0 && pos != end) {
      BufferChunk bc = buffer_getChunk(inBuf, pos, end - pos);
      assert(bc.len <= (end - pos));
      assert(bc.len); /* better not be 0-sized BufferChunks! */
      assert(bc.ptr);

      zstrm.next_in = (char *) bc.ptr;
      zstrm.avail_in = bc.len;

      pos += bc.len;
    }

    /* Now, rerun deflate until deflate can produce no more output;
       at least once and maybe lots of times
    */
    do {
      int leave = 1;

      log_trace(DBG_COMPRESS, 
		format("deflate info: (0x%x %d) (0x%x %d) %d %d\n",
		       (uint32_t)zstrm.next_in, zstrm.avail_in,
		       (uint32_t)zstrm.next_out, zstrm.avail_out,
		       (int)pos, (int)end
        ));
      /* 
         If pos == end, then we're completly out of stuff to compress. That
         means, it's time to use Z_FINISH and get the hell out of town.
         Contrary to a comment that was here before, avail_in==0 does NOT
         (necessarily) imply that we are out of data to compress; it may be
         that we are repeatedly looping waiting for zlib to finish outputting
         all of it's data.
      */
      if(pos == end)
        result = deflate(&zstrm, Z_FINISH);
      else
        result = deflate(&zstrm, Z_NO_FLUSH);

      /* Read comment in buffer_decompress for why we are ignoring Z_BUF_ERROR */
      if (result != Z_OK && result != Z_STREAM_END && result != Z_BUF_ERROR)
        THROW(ExBadValue, format("Deflate failed (error %d)", result));

      buffer_append(outBuf, buffer, ZLIB_BUFFER_SIZE - zstrm.avail_out);
      zstrm.next_out = buffer;

      /* If avail_out is == 0, then we have to rerun deflate because there
         might be more output remaining */
      if(zstrm.avail_out == 0)
        leave = 0;

      zstrm.avail_out = ZLIB_BUFFER_SIZE;

      if(leave)
        break;
    } while(1);
  }

  /* Simple check */
  assert(pos == end);

  result = deflateEnd(&zstrm);
  if(result != Z_OK)
    THROW(ExBadValue, format("Deflate failed (error %d)", result));

  /* A better check, but maybe a bit expensive for every compression call */
#if 0
    {
      /* zstrm.adler is the Adler32 checksum of all of the uncompressed data
         that was input into deflate(); compare that with the Adler32 of the
         buffer. This will catch any buffering errors
      */
      uint32_t adler = zstrm.adler;
      uint32_t our_adler = adler32(0, 0, 0);
      char* x = buffer_asString(inBuf);
      our_adler = adler32(our_adler, x, buffer_length(inBuf));
      assert(adler == our_adler);
    }
#endif

  return outBuf;
}

Buffer *
buffer_decompress(Buffer *inBuf)
{
  char buffer[ZLIB_BUFFER_SIZE];
  z_stream zstrm;
  ocmoff_t pos = 0;
  ocmoff_t end = buffer_length(inBuf);
  Buffer *outBuf = buffer_create();
  int result = Z_OK;

  memset(&zstrm, 0, sizeof(zstrm));

  zstrm.zalloc = Z_NULL;
  zstrm.zfree = Z_NULL;
  zstrm.opaque = Z_NULL;
  zstrm.total_out= 0;
  zstrm.total_in= 0;
  zstrm.data_type = Z_BINARY;

  zstrm.next_out = buffer;
  zstrm.avail_out = ZLIB_BUFFER_SIZE;

  zstrm.avail_in = 0;

  log_trace(DBG_COMPRESS, format("Decompressing %d\n", (int)buffer_length(inBuf)));

  if(inflateInit(&zstrm) != Z_OK)
    THROW(ExBadValue, "Could not initialize zlib: inflateInit");

  while(result != Z_STREAM_END)
  {
    /* add more if the zlib buffer is empty and there is more in inBuf */
    if (zstrm.avail_in == 0 && pos != end) {
      BufferChunk bc = buffer_getChunk(inBuf, pos, end - pos);
      assert(bc.len <= (end - pos));

      zstrm.next_in = (char *) bc.ptr;
      zstrm.avail_in = bc.len;

      pos += bc.len;
    }

    /* Now, rerun inflate until inflate can produce no more output;
       at least once and maybe lots of times
    */
    do {
      int leave = 1;

      log_trace(DBG_COMPRESS, format("inflate info: (0x%x %d) (0x%x %d) %d %d\n",
			  (uint32_t)zstrm.next_in, zstrm.avail_in,
			  (uint32_t)zstrm.next_out, zstrm.avail_out,
			  (int)pos, (int)end
			  ));

      result = inflate(&zstrm, Z_NO_FLUSH);

      /*
        There is a most annoying bug that seems to pop up on a quite
        regular basis, where inflate returns with avail_out == 0. In this
        case, zlib.h counsels that we should call inflate again after making
        more room. However, it seems that if there is, in fact, no more output,
        zlib will decide WE made a mistake, despite the fact that there is
        no way for us to know either way if more data remains, without calling
        inflate.

        Anyway, what we do is basically ignore an error return of Z_BUF_ERROR,
        because it is not a real error anyway (all it is is zlib complaining
        that it couldn't do anything when you called inflate).
      */

      if (result != Z_OK && result != Z_STREAM_END && result != Z_BUF_ERROR)
        THROW(ExBadValue, format("Inflate failed (error %d)", result));

      /* Make sure that, if it did return this, it didn't touch the buffers.
         Then leave, since we're done with the data we had, and need to get
         some more. */
      if(result == Z_BUF_ERROR)
      {
        xassert(zstrm.avail_out == ZLIB_BUFFER_SIZE);
        break;
      }

      /* If the stream is ended, the buffer better be empty */
      if(result == Z_STREAM_END && pos != end)
        THROW(ExBadValue, "Inflate failed: Garbage at end of Buffer");

      buffer_append(outBuf, buffer, ZLIB_BUFFER_SIZE - zstrm.avail_out);
      zstrm.next_out = buffer;

      /* If avail_out is == 0, then we have to rerun inflate because there
         might be more output remaining */
      if(zstrm.avail_out == 0)
        leave = 0;

      zstrm.avail_out = ZLIB_BUFFER_SIZE;

      if(leave)
        break;
    } while(1);
  }

  result = inflateEnd(&zstrm);
  if(result != Z_OK)
    THROW(ExBadValue, format("Inflate failed (error %d)", result));

  return outBuf;
}

const char *
buffer_getContentTrueName(const Buffer *buf)
{
  const char *hex;
  ocmoff_t end = buffer_length(buf);
  ocmoff_t pos = 0;
  OPENCM_SHA *sha = sha_create();

  while (pos < end) {
    BufferChunk bc = buffer_getChunk(buf, pos, end - pos);

    sha_append(sha, bc.len, bc.ptr);
    pos += bc.len;
  }

  sha_finish(sha);
  
  hex = sha_hexdigest(sha);
  return truename_FromHash(hex);
}
