/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

#include <algorithm>
#include "SeekableZStream.h"
#include "Logging.h"

bool
SeekableZStream::Init(const void *buf, size_t length)
{
  const SeekableZStreamHeader *header = SeekableZStreamHeader::validate(buf);
  if (!header) {
    ERROR("Not a seekable zstream");
    return false;
  }

  buffer = reinterpret_cast<const unsigned char *>(buf);
  totalSize = header->totalSize;
  chunkSize = header->chunkSize;
  lastChunkSize = header->lastChunkSize;
  windowBits = header->windowBits;
  dictionary.Init(buffer + sizeof(SeekableZStreamHeader), header->dictSize);
  offsetTable.Init(buffer + sizeof(SeekableZStreamHeader) + header->dictSize,
                   header->nChunks);
  filter = GetFilter(header->filter);

  /* Sanity check */
  if ((chunkSize == 0) ||
      (!IsPageAlignedSize(chunkSize)) ||
      (chunkSize > 8 * PageSize()) ||
      (offsetTable.numElements() < 1) ||
      (lastChunkSize == 0) ||
      (lastChunkSize > chunkSize) ||
      (length < totalSize)) {
    ERROR("Malformed or broken seekable zstream");
    return false;
  }

  return true;
}

bool
SeekableZStream::Decompress(void *where, size_t chunk, size_t length)
{
  while (length) {
    size_t len = std::min(length, static_cast<size_t>(chunkSize));
    if (!DecompressChunk(where, chunk, len))
      return false;
    where = reinterpret_cast<unsigned char *>(where) + len;
    length -= len;
    chunk++;
  }
  return true;
}

bool
SeekableZStream::DecompressChunk(void *where, size_t chunk, size_t length)
{
  if (chunk >= offsetTable.numElements()) {
    ERROR("DecompressChunk: chunk #%" PRIdSize " out of range [0-%" PRIdSize ")",
        chunk, offsetTable.numElements());
    return false;
  }

  bool isLastChunk = (chunk == offsetTable.numElements() - 1);

  size_t chunkLen = isLastChunk ? lastChunkSize : chunkSize;

  if (length == 0 || length > chunkLen)
    length = chunkLen;

  DEBUG_LOG("DecompressChunk #%" PRIdSize " @%p (%" PRIdSize "/% " PRIdSize ")",
        chunk, where, length, chunkLen);
  zxx_stream zStream(&allocator);
  zStream.avail_in = (isLastChunk ? totalSize : uint32_t(offsetTable[chunk + 1]))
                     - uint32_t(offsetTable[chunk]);
  zStream.next_in = const_cast<Bytef *>(buffer + uint32_t(offsetTable[chunk]));
  zStream.avail_out = length;
  zStream.next_out = reinterpret_cast<Bytef *>(where);

  /* Decompress chunk */
  if (inflateInit2(&zStream, windowBits) != Z_OK) {
    ERROR("inflateInit failed: %s", zStream.msg);
    return false;
  }
  if (dictionary && inflateSetDictionary(&zStream, dictionary,
                                         dictionary.numElements()) != Z_OK) {
    ERROR("inflateSetDictionary failed: %s", zStream.msg);
    return false;
  }
  if (inflate(&zStream, (length == chunkLen) ? Z_FINISH : Z_SYNC_FLUSH)
      != (length == chunkLen) ? Z_STREAM_END : Z_OK) {
    ERROR("inflate failed: %s", zStream.msg);
    return false;
  }
  if (inflateEnd(&zStream) != Z_OK) {
    ERROR("inflateEnd failed: %s", zStream.msg);
    return false;
  }
  if (filter)
    filter(chunk * chunkSize, UNFILTER, (unsigned char *)where, chunkLen);

  return true;
}

/* Branch/Call/Jump conversion filter for Thumb, derived from xz-utils
 * by Igor Pavlov and Lasse Collin, published in the public domain */
static void
BCJ_Thumb_filter(off_t offset, SeekableZStream::FilterDirection dir,
                 unsigned char *buf, size_t size)
{
  size_t i;
  for (i = 0; i + 4 <= size; i += 2) {
    if ((buf[i + 1] & 0xf8) == 0xf0 && (buf[i + 3] & 0xf8) == 0xf8) {
      uint32_t src = (buf[i] << 11)
                     | ((buf[i + 1] & 0x07) << 19)
                     | buf[i + 2]
                     | ((buf[i + 3] & 0x07) << 8);
      src <<= 1;
      uint32_t dest;
      if (dir == SeekableZStream::FILTER)
        dest = offset + (uint32_t)(i) + 4 + src;
      else
        dest = src - (offset + (uint32_t)(i) + 4);

      dest >>= 1;
      buf[i] = dest >> 11;
      buf[i + 1] = 0xf0 | ((dest >> 19) & 0x07);
      buf[i + 2] = dest;
      buf[i + 3] = 0xf8 | ((dest >> 8) & 0x07);
      i += 2;
    }
  }
}

/* Branch/Call/Jump conversion filter for ARM, derived from xz-utils
 * by Igor Pavlov and Lasse Collin, published in the public domain */
static void
BCJ_ARM_filter(off_t offset, SeekableZStream::FilterDirection dir,
               unsigned char *buf, size_t size)
{
  size_t i;
  for (i = 0; i + 4 <= size; i += 4) {
    if (buf[i + 3] == 0xeb) {
      uint32_t src = buf[i]
                     | (buf[i + 1] << 8)
                     | (buf[i + 2] << 16);
      src <<= 2;
      uint32_t dest;
      if (dir == SeekableZStream::FILTER)
        dest = offset + (uint32_t)(i) + 8 + src;
      else
        dest = src - (offset + (uint32_t)(i) + 8);

      dest >>= 2;
      buf[i] = dest;
      buf[i + 1] = dest >> 8;
      buf[i + 2] = dest >> 16;
    }
  }
}

/* Branch/Call/Jump conversion filter for x86, derived from xz-utils
 * by Igor Pavlov and Lasse Collin, published in the public domain */

#define Test86MSByte(b) ((b) == 0 || (b) == 0xff)

static void
BCJ_X86_filter(off_t offset, SeekableZStream::FilterDirection dir,
               unsigned char *buf, size_t size)
{
  static const bool MASK_TO_ALLOWED_STATUS[8] =
    { true, true, true, false, true, false, false, false };

  static const uint32_t MASK_TO_BIT_NUMBER[8] =
    { 0, 1, 2, 2, 3, 3, 3, 3 };

  uint32_t prev_mask = 0;
  uint32_t prev_pos = 0;

  for (size_t i = 0; i + 5 <= size;) {
    uint8_t b = buf[i];
    if (b != 0xe8 && b != 0xe9) {
      ++i;
      continue;
    }

    const uint32_t off = offset + (uint32_t)(i) - prev_pos;
    prev_pos = offset + (uint32_t)(i);

    if (off > 5) {
      prev_mask = 0;
    } else {
      for (uint32_t i = 0; i < off; ++i) {
        prev_mask &= 0x77;
        prev_mask <<= 1;
      }
    }

    b = buf[i + 4];

    if (Test86MSByte(b) && MASK_TO_ALLOWED_STATUS[(prev_mask >> 1) & 0x7]
        && (prev_mask >> 1) < 0x10) {

      uint32_t src = ((uint32_t)(b) << 24)
                     | ((uint32_t)(buf[i + 3]) << 16)
                     | ((uint32_t)(buf[i + 2]) << 8)
                     | (buf[i + 1]);

      uint32_t dest;
      while (true) {
        if (dir == SeekableZStream::FILTER)
          dest = src + (offset + (uint32_t)(i) + 5);
        else
          dest = src - (offset + (uint32_t)(i) + 5);

        if (prev_mask == 0)
          break;

        const uint32_t i = MASK_TO_BIT_NUMBER[prev_mask >> 1];

        b = (uint8_t)(dest >> (24 - i * 8));

        if (!Test86MSByte(b))
          break;

        src = dest ^ ((1 << (32 - i * 8)) - 1);
      }

      buf[i + 4] = (uint8_t)(~(((dest >> 24) & 1) - 1));
      buf[i + 3] = (uint8_t)(dest >> 16);
      buf[i + 2] = (uint8_t)(dest >> 8);
      buf[i + 1] = (uint8_t)(dest);
      i += 5;
      prev_mask = 0;

    } else {
      ++i;
      prev_mask |= 1;
      if (Test86MSByte(b))
        prev_mask |= 0x10;
    }
  }
}

SeekableZStream::ZStreamFilter
SeekableZStream::GetFilter(SeekableZStream::FilterId id)
{
  switch (id) {
  case BCJ_THUMB:
    return BCJ_Thumb_filter;
  case BCJ_ARM:
    return BCJ_ARM_filter;
  case BCJ_X86:
    return BCJ_X86_filter;
  default:
    return nullptr;
  }
  return nullptr;
}
