/* 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 "mozilla/SnappyFrameUtils.h"

#include "crc32c.h"
#include "mozilla/EndianUtils.h"
#include "nsDebug.h"
#include "snappy/snappy.h"

namespace {

using mozilla::NativeEndian;
using mozilla::detail::SnappyFrameUtils;

SnappyFrameUtils::ChunkType ReadChunkType(uint8_t aByte) {
  if (aByte == 0xff) {
    return SnappyFrameUtils::StreamIdentifier;
  } else if (aByte == 0x00) {
    return SnappyFrameUtils::CompressedData;
  } else if (aByte == 0x01) {
    return SnappyFrameUtils::UncompressedData;
  } else if (aByte == 0xfe) {
    return SnappyFrameUtils::Padding;
  }

  return SnappyFrameUtils::Reserved;
}

void WriteChunkType(char* aDest, SnappyFrameUtils::ChunkType aType) {
  unsigned char* dest = reinterpret_cast<unsigned char*>(aDest);
  if (aType == SnappyFrameUtils::StreamIdentifier) {
    *dest = 0xff;
  } else if (aType == SnappyFrameUtils::CompressedData) {
    *dest = 0x00;
  } else if (aType == SnappyFrameUtils::UncompressedData) {
    *dest = 0x01;
  } else if (aType == SnappyFrameUtils::Padding) {
    *dest = 0xfe;
  } else {
    *dest = 0x02;
  }
}

void WriteUInt24(char* aBuf, uint32_t aVal) {
  MOZ_ASSERT(!(aVal & 0xff000000));
  uint32_t tmp = NativeEndian::swapToLittleEndian(aVal);
  memcpy(aBuf, &tmp, 3);
}

uint32_t ReadUInt24(const char* aBuf) {
  uint32_t val = 0;
  memcpy(&val, aBuf, 3);
  return NativeEndian::swapFromLittleEndian(val);
}

// This mask is explicitly defined in the snappy framing_format.txt file.
uint32_t MaskChecksum(uint32_t aValue) {
  return ((aValue >> 15) | (aValue << 17)) + 0xa282ead8;
}

}  // namespace

namespace mozilla {
namespace detail {

using mozilla::LittleEndian;

// static
nsresult SnappyFrameUtils::WriteStreamIdentifier(char* aDest,
                                                 size_t aDestLength,
                                                 size_t* aBytesWrittenOut) {
  if (NS_WARN_IF(aDestLength < (kHeaderLength + kStreamIdentifierDataLength))) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  WriteChunkType(aDest, StreamIdentifier);
  aDest[1] = 0x06;  // Data length
  aDest[2] = 0x00;
  aDest[3] = 0x00;
  aDest[4] = 0x73;  // "sNaPpY"
  aDest[5] = 0x4e;
  aDest[6] = 0x61;
  aDest[7] = 0x50;
  aDest[8] = 0x70;
  aDest[9] = 0x59;

  static_assert(kHeaderLength + kStreamIdentifierDataLength == 10,
                "StreamIdentifier chunk should be exactly 10 bytes long");
  *aBytesWrittenOut = kHeaderLength + kStreamIdentifierDataLength;

  return NS_OK;
}

// static
nsresult SnappyFrameUtils::WriteCompressedData(char* aDest, size_t aDestLength,
                                               const char* aData,
                                               size_t aDataLength,
                                               size_t* aBytesWrittenOut) {
  *aBytesWrittenOut = 0;

  size_t neededLength = MaxCompressedBufferLength(aDataLength);
  if (NS_WARN_IF(aDestLength < neededLength)) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  size_t offset = 0;

  WriteChunkType(aDest, CompressedData);
  offset += kChunkTypeLength;

  // Skip length for now and write it out after we know the compressed length.
  size_t lengthOffset = offset;
  offset += kChunkLengthLength;

  uint32_t crc = ComputeCrc32c(
      ~0, reinterpret_cast<const unsigned char*>(aData), aDataLength);
  uint32_t maskedCrc = MaskChecksum(crc);
  LittleEndian::writeUint32(aDest + offset, maskedCrc);
  offset += kCRCLength;

  size_t compressedLength;
  snappy::RawCompress(aData, aDataLength, aDest + offset, &compressedLength);

  // Go back and write the data length.
  size_t dataLength = compressedLength + kCRCLength;
  WriteUInt24(aDest + lengthOffset, dataLength);

  *aBytesWrittenOut = kHeaderLength + dataLength;

  return NS_OK;
}

// static
nsresult SnappyFrameUtils::ParseHeader(const char* aSource,
                                       size_t aSourceLength,
                                       ChunkType* aTypeOut,
                                       size_t* aDataLengthOut) {
  if (NS_WARN_IF(aSourceLength < kHeaderLength)) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  *aTypeOut = ReadChunkType(aSource[0]);
  *aDataLengthOut = ReadUInt24(aSource + kChunkTypeLength);

  return NS_OK;
}

// static
nsresult SnappyFrameUtils::ParseData(char* aDest, size_t aDestLength,
                                     ChunkType aType, const char* aData,
                                     size_t aDataLength,
                                     size_t* aBytesWrittenOut,
                                     size_t* aBytesReadOut) {
  switch (aType) {
    case StreamIdentifier:
      return ParseStreamIdentifier(aDest, aDestLength, aData, aDataLength,
                                   aBytesWrittenOut, aBytesReadOut);

    case CompressedData:
      return ParseCompressedData(aDest, aDestLength, aData, aDataLength,
                                 aBytesWrittenOut, aBytesReadOut);

    // TODO: support other snappy chunk types
    default:
      MOZ_ASSERT_UNREACHABLE("Unsupported snappy framing chunk type.");
      return NS_ERROR_NOT_IMPLEMENTED;
  }
}

// static
nsresult SnappyFrameUtils::ParseStreamIdentifier(char*, size_t,
                                                 const char* aData,
                                                 size_t aDataLength,
                                                 size_t* aBytesWrittenOut,
                                                 size_t* aBytesReadOut) {
  *aBytesWrittenOut = 0;
  *aBytesReadOut = 0;
  if (NS_WARN_IF(aDataLength != kStreamIdentifierDataLength ||
                 aData[0] != 0x73 || aData[1] != 0x4e || aData[2] != 0x61 ||
                 aData[3] != 0x50 || aData[4] != 0x70 || aData[5] != 0x59)) {
    return NS_ERROR_CORRUPTED_CONTENT;
  }
  *aBytesReadOut = aDataLength;
  return NS_OK;
}

// static
nsresult SnappyFrameUtils::ParseCompressedData(char* aDest, size_t aDestLength,
                                               const char* aData,
                                               size_t aDataLength,
                                               size_t* aBytesWrittenOut,
                                               size_t* aBytesReadOut) {
  *aBytesWrittenOut = 0;
  *aBytesReadOut = 0;
  size_t offset = 0;

  // A CompressedData chunk must contain at least the masked CRC.
  if (NS_WARN_IF(aDataLength < kCRCLength)) {
    return NS_ERROR_CORRUPTED_CONTENT;
  }

  uint32_t readCrc = LittleEndian::readUint32(aData + offset);
  offset += kCRCLength;

  size_t uncompressedLength;
  if (NS_WARN_IF(!snappy::GetUncompressedLength(
          aData + offset, aDataLength - offset, &uncompressedLength))) {
    return NS_ERROR_CORRUPTED_CONTENT;
  }

  if (NS_WARN_IF(aDestLength < uncompressedLength)) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  if (NS_WARN_IF(!snappy::RawUncompress(aData + offset, aDataLength - offset,
                                        aDest))) {
    return NS_ERROR_CORRUPTED_CONTENT;
  }

  uint32_t crc = ComputeCrc32c(
      ~0, reinterpret_cast<const unsigned char*>(aDest), uncompressedLength);
  uint32_t maskedCrc = MaskChecksum(crc);
  if (NS_WARN_IF(readCrc != maskedCrc)) {
    return NS_ERROR_CORRUPTED_CONTENT;
  }

  *aBytesWrittenOut = uncompressedLength;
  *aBytesReadOut = aDataLength;

  return NS_OK;
}

// static
size_t SnappyFrameUtils::MaxCompressedBufferLength(size_t aSourceLength) {
  size_t neededLength = kHeaderLength;
  neededLength += kCRCLength;
  neededLength += snappy::MaxCompressedLength(aSourceLength);
  return neededLength;
}

}  // namespace detail
}  // namespace mozilla
