/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#define LOG_TAG "ZIPARCHIVE"

// Read-only stream access to Zip Archive entries.
#include <errno.h>
#include <inttypes.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

#include <memory>
#include <vector>

#include <android-base/file.h>
#include <log/log.h>

#include <ziparchive/zip_archive.h>
#include <ziparchive/zip_archive_stream_entry.h>
#include <zlib.h>

#include "zip_archive_private.h"

static constexpr size_t kBufSize = 65535;

bool ZipArchiveStreamEntry::Init(const ZipEntry& entry) {
  crc32_ = entry.crc32;
  offset_ = entry.offset;
  return true;
}

class ZipArchiveStreamEntryUncompressed : public ZipArchiveStreamEntry {
 public:
  explicit ZipArchiveStreamEntryUncompressed(ZipArchiveHandle handle)
      : ZipArchiveStreamEntry(handle) {}
  virtual ~ZipArchiveStreamEntryUncompressed() {}

  const std::vector<uint8_t>* Read() override;

  bool Verify() override;

 protected:
  bool Init(const ZipEntry& entry) override;

  uint32_t length_ = 0u;

 private:
  std::vector<uint8_t> data_;
  uint32_t computed_crc32_ = 0u;
};

bool ZipArchiveStreamEntryUncompressed::Init(const ZipEntry& entry) {
  if (!ZipArchiveStreamEntry::Init(entry)) {
    return false;
  }

  length_ = entry.uncompressed_length;

  data_.resize(kBufSize);
  computed_crc32_ = 0;

  return true;
}

const std::vector<uint8_t>* ZipArchiveStreamEntryUncompressed::Read() {
  if (length_ == 0) {
    return nullptr;
  }

  size_t bytes = (length_ > data_.size()) ? data_.size() : length_;
  ZipArchive* archive = reinterpret_cast<ZipArchive*>(handle_);
  errno = 0;
  if (!archive->mapped_zip.ReadAtOffset(data_.data(), bytes, offset_)) {
    if (errno != 0) {
      ALOGE("Error reading from archive fd: %s", strerror(errno));
    } else {
      ALOGE("Short read of zip file, possibly corrupted zip?");
    }
    length_ = 0;
    return nullptr;
  }

  if (bytes < data_.size()) {
    data_.resize(bytes);
  }
  computed_crc32_ = crc32(computed_crc32_, data_.data(), data_.size());
  length_ -= bytes;
  offset_ += bytes;
  return &data_;
}

bool ZipArchiveStreamEntryUncompressed::Verify() {
  return length_ == 0 && crc32_ == computed_crc32_;
}

class ZipArchiveStreamEntryCompressed : public ZipArchiveStreamEntry {
 public:
  explicit ZipArchiveStreamEntryCompressed(ZipArchiveHandle handle)
      : ZipArchiveStreamEntry(handle) {}
  virtual ~ZipArchiveStreamEntryCompressed();

  const std::vector<uint8_t>* Read() override;

  bool Verify() override;

 protected:
  bool Init(const ZipEntry& entry) override;

 private:
  bool z_stream_init_ = false;
  z_stream z_stream_;
  std::vector<uint8_t> in_;
  std::vector<uint8_t> out_;
  uint32_t uncompressed_length_ = 0u;
  uint32_t compressed_length_ = 0u;
  uint32_t computed_crc32_ = 0u;
};

// This method is using libz macros with old-style-casts
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wold-style-cast"
static inline int zlib_inflateInit2(z_stream* stream, int window_bits) {
  return inflateInit2(stream, window_bits);
}
#pragma GCC diagnostic pop

bool ZipArchiveStreamEntryCompressed::Init(const ZipEntry& entry) {
  if (!ZipArchiveStreamEntry::Init(entry)) {
    return false;
  }

  // Initialize the zlib stream struct.
  memset(&z_stream_, 0, sizeof(z_stream_));
  z_stream_.zalloc = Z_NULL;
  z_stream_.zfree = Z_NULL;
  z_stream_.opaque = Z_NULL;
  z_stream_.next_in = nullptr;
  z_stream_.avail_in = 0;
  z_stream_.avail_out = 0;
  z_stream_.data_type = Z_UNKNOWN;

  // Use the undocumented "negative window bits" feature to tell zlib
  // that there's no zlib header waiting for it.
  int zerr = zlib_inflateInit2(&z_stream_, -MAX_WBITS);
  if (zerr != Z_OK) {
    if (zerr == Z_VERSION_ERROR) {
      ALOGE("Installed zlib is not compatible with linked version (%s)", ZLIB_VERSION);
    } else {
      ALOGE("Call to inflateInit2 failed (zerr=%d)", zerr);
    }

    return false;
  }

  z_stream_init_ = true;

  uncompressed_length_ = entry.uncompressed_length;
  compressed_length_ = entry.compressed_length;

  out_.resize(kBufSize);
  in_.resize(kBufSize);

  computed_crc32_ = 0;

  return true;
}

ZipArchiveStreamEntryCompressed::~ZipArchiveStreamEntryCompressed() {
  if (z_stream_init_) {
    inflateEnd(&z_stream_);
    z_stream_init_ = false;
  }
}

bool ZipArchiveStreamEntryCompressed::Verify() {
  return z_stream_init_ && uncompressed_length_ == 0 && compressed_length_ == 0 &&
         crc32_ == computed_crc32_;
}

const std::vector<uint8_t>* ZipArchiveStreamEntryCompressed::Read() {
  if (z_stream_.avail_out == 0) {
    z_stream_.next_out = out_.data();
    z_stream_.avail_out = out_.size();
    ;
  }

  while (true) {
    if (z_stream_.avail_in == 0) {
      if (compressed_length_ == 0) {
        return nullptr;
      }
      size_t bytes = (compressed_length_ > in_.size()) ? in_.size() : compressed_length_;
      ZipArchive* archive = reinterpret_cast<ZipArchive*>(handle_);
      errno = 0;
      if (!archive->mapped_zip.ReadAtOffset(in_.data(), bytes, offset_)) {
        if (errno != 0) {
          ALOGE("Error reading from archive fd: %s", strerror(errno));
        } else {
          ALOGE("Short read of zip file, possibly corrupted zip?");
        }
        return nullptr;
      }

      compressed_length_ -= bytes;
      offset_ += bytes;
      z_stream_.next_in = in_.data();
      z_stream_.avail_in = bytes;
    }

    int zerr = inflate(&z_stream_, Z_NO_FLUSH);
    if (zerr != Z_OK && zerr != Z_STREAM_END) {
      ALOGE("inflate zerr=%d (nIn=%p aIn=%u nOut=%p aOut=%u)", zerr, z_stream_.next_in,
            z_stream_.avail_in, z_stream_.next_out, z_stream_.avail_out);
      return nullptr;
    }

    if (z_stream_.avail_out == 0) {
      uncompressed_length_ -= out_.size();
      computed_crc32_ = crc32(computed_crc32_, out_.data(), out_.size());
      return &out_;
    }
    if (zerr == Z_STREAM_END) {
      if (z_stream_.avail_out != 0) {
        // Resize the vector down to the actual size of the data.
        out_.resize(out_.size() - z_stream_.avail_out);
        computed_crc32_ = crc32(computed_crc32_, out_.data(), out_.size());
        uncompressed_length_ -= out_.size();
        return &out_;
      }
      return nullptr;
    }
  }
  return nullptr;
}

class ZipArchiveStreamEntryRawCompressed : public ZipArchiveStreamEntryUncompressed {
 public:
  explicit ZipArchiveStreamEntryRawCompressed(ZipArchiveHandle handle)
      : ZipArchiveStreamEntryUncompressed(handle) {}
  virtual ~ZipArchiveStreamEntryRawCompressed() {}

  bool Verify() override;

 protected:
  bool Init(const ZipEntry& entry) override;
};

bool ZipArchiveStreamEntryRawCompressed::Init(const ZipEntry& entry) {
  if (!ZipArchiveStreamEntryUncompressed::Init(entry)) {
    return false;
  }
  length_ = entry.compressed_length;

  return true;
}

bool ZipArchiveStreamEntryRawCompressed::Verify() {
  return length_ == 0;
}

ZipArchiveStreamEntry* ZipArchiveStreamEntry::Create(ZipArchiveHandle handle,
                                                     const ZipEntry& entry) {
  ZipArchiveStreamEntry* stream = nullptr;
  if (entry.method != kCompressStored) {
    stream = new ZipArchiveStreamEntryCompressed(handle);
  } else {
    stream = new ZipArchiveStreamEntryUncompressed(handle);
  }
  if (stream && !stream->Init(entry)) {
    delete stream;
    stream = nullptr;
  }

  return stream;
}

ZipArchiveStreamEntry* ZipArchiveStreamEntry::CreateRaw(ZipArchiveHandle handle,
                                                        const ZipEntry& entry) {
  ZipArchiveStreamEntry* stream = nullptr;
  if (entry.method == kCompressStored) {
    // Not compressed, don't need to do anything special.
    stream = new ZipArchiveStreamEntryUncompressed(handle);
  } else {
    stream = new ZipArchiveStreamEntryRawCompressed(handle);
  }
  if (stream && !stream->Init(entry)) {
    delete stream;
    stream = nullptr;
  }
  return stream;
}
