/*
 * Copyright (C) 2017 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.
 */

#include "format/Container.h"

#include "android-base/scopeguard.h"
#include "android-base/stringprintf.h"

using ::android::base::StringPrintf;
using ::google::protobuf::io::CodedInputStream;
using ::google::protobuf::io::CodedOutputStream;
using ::google::protobuf::io::ZeroCopyOutputStream;

namespace aapt {

constexpr const static uint32_t kContainerFormatMagic = 0x54504141u;
constexpr const static uint32_t kContainerFormatVersion = 1u;

ContainerWriter::ContainerWriter(ZeroCopyOutputStream* out, size_t entry_count)
    : out_(out), total_entry_count_(entry_count), current_entry_count_(0u) {
  CodedOutputStream coded_out(out_);

  // Write the magic.
  coded_out.WriteLittleEndian32(kContainerFormatMagic);

  // Write the version.
  coded_out.WriteLittleEndian32(kContainerFormatVersion);

  // Write the total number of entries.
  coded_out.WriteLittleEndian32(static_cast<uint32_t>(total_entry_count_));

  if (coded_out.HadError()) {
    error_ = "failed writing container format header";
  }
}

inline static void WritePadding(int padding, CodedOutputStream* out) {
  if (padding < 4) {
    const uint32_t zero = 0u;
    out->WriteRaw(&zero, padding);
  }
}

bool ContainerWriter::AddResTableEntry(const pb::ResourceTable& table) {
  if (current_entry_count_ >= total_entry_count_) {
    error_ = "too many entries being serialized";
    return false;
  }
  current_entry_count_++;

  CodedOutputStream coded_out(out_);

  // Write the type.
  coded_out.WriteLittleEndian32(kResTable);

  // Write the aligned size.
  const ::google::protobuf::uint64 size = table.ByteSize();
  const int padding = 4 - (size % 4);
  coded_out.WriteLittleEndian64(size);

  // Write the table.
  table.SerializeWithCachedSizes(&coded_out);

  // Write the padding.
  WritePadding(padding, &coded_out);

  if (coded_out.HadError()) {
    error_ = "failed writing to output";
    return false;
  }
  return true;
}

bool ContainerWriter::AddResFileEntry(const pb::internal::CompiledFile& file,
                                      io::KnownSizeInputStream* in) {
  if (current_entry_count_ >= total_entry_count_) {
    error_ = "too many entries being serialized";
    return false;
  }
  current_entry_count_++;

  constexpr const static int kResFileEntryHeaderSize = 12;

  CodedOutputStream coded_out(out_);

  // Write the type.
  coded_out.WriteLittleEndian32(kResFile);

  // Write the aligned size.
  const ::google::protobuf::uint32 header_size = file.ByteSize();
  const int header_padding = 4 - (header_size % 4);
  const ::google::protobuf::uint64 data_size = in->TotalSize();
  const int data_padding = 4 - (data_size % 4);
  coded_out.WriteLittleEndian64(kResFileEntryHeaderSize + header_size + header_padding + data_size +
                                data_padding);

  // Write the res file header size.
  coded_out.WriteLittleEndian32(header_size);

  // Write the data payload size.
  coded_out.WriteLittleEndian64(data_size);

  // Write the header.
  file.SerializeToCodedStream(&coded_out);

  WritePadding(header_padding, &coded_out);

  // Write the data payload. We need to call Trim() since we are going to write to the underlying
  // ZeroCopyOutputStream.
  coded_out.Trim();

  // Check at this point if there were any errors.
  if (coded_out.HadError()) {
    error_ = "failed writing to output";
    return false;
  }

  if (!io::Copy(out_, in)) {
    if (in->HadError()) {
      std::ostringstream error;
      error << "failed reading from input: " << in->GetError();
      error_ = error.str();
    } else {
      error_ = "failed writing to output";
    }
    return false;
  }
  WritePadding(data_padding, &coded_out);

  if (coded_out.HadError()) {
    error_ = "failed writing to output";
    return false;
  }
  return true;
}

bool ContainerWriter::HadError() const {
  return !error_.empty();
}

std::string ContainerWriter::GetError() const {
  return error_;
}

static bool AlignRead(CodedInputStream* in) {
  const int padding = 4 - (in->CurrentPosition() % 4);
  if (padding < 4) {
    return in->Skip(padding);
  }
  return true;
}

ContainerReaderEntry::ContainerReaderEntry(ContainerReader* reader) : reader_(reader) {
}

ContainerEntryType ContainerReaderEntry::Type() const {
  return type_;
}

bool ContainerReaderEntry::GetResTable(pb::ResourceTable* out_table) {
  CHECK(type_ == ContainerEntryType::kResTable) << "reading a kResTable when the type is kResFile";
  if (length_ > std::numeric_limits<int>::max()) {
    reader_->error_ = StringPrintf("entry length %zu is too large", length_);
    return false;
  }

  CodedInputStream& coded_in = reader_->coded_in_;

  const CodedInputStream::Limit limit = coded_in.PushLimit(static_cast<int>(length_));
  auto guard = ::android::base::make_scope_guard([&]() { coded_in.PopLimit(limit); });

  if (!out_table->ParseFromCodedStream(&coded_in)) {
    reader_->error_ = "failed to parse ResourceTable";
    return false;
  }
  return true;
}

bool ContainerReaderEntry::GetResFileOffsets(pb::internal::CompiledFile* out_file,
                                             off64_t* out_offset, size_t* out_len) {
  CHECK(type_ == ContainerEntryType::kResFile) << "reading a kResFile when the type is kResTable";

  CodedInputStream& coded_in = reader_->coded_in_;

  // Read the ResFile header.
  ::google::protobuf::uint32 header_length;
  if (!coded_in.ReadLittleEndian32(&header_length)) {
    std::ostringstream error;
    error << "failed to read header length from input: " << reader_->in_->GetError();
    reader_->error_ = error.str();
    return false;
  }

  ::google::protobuf::uint64 data_length;
  if (!coded_in.ReadLittleEndian64(&data_length)) {
    std::ostringstream error;
    error << "failed to read data length from input: " << reader_->in_->GetError();
    reader_->error_ = error.str();
    return false;
  }

  if (header_length > std::numeric_limits<int>::max()) {
    std::ostringstream error;
    error << "header length " << header_length << " is too large";
    reader_->error_ = error.str();
    return false;
  }

  if (data_length > std::numeric_limits<size_t>::max()) {
    std::ostringstream error;
    error << "data length " << data_length << " is too large";
    reader_->error_ = error.str();
    return false;
  }

  {
    const CodedInputStream::Limit limit = coded_in.PushLimit(static_cast<int>(header_length));
    auto guard = ::android::base::make_scope_guard([&]() { coded_in.PopLimit(limit); });

    if (!out_file->ParseFromCodedStream(&coded_in)) {
      reader_->error_ = "failed to parse CompiledFile header";
      return false;
    }
  }

  AlignRead(&coded_in);

  *out_offset = coded_in.CurrentPosition();
  *out_len = data_length;

  coded_in.Skip(static_cast<int>(data_length));
  AlignRead(&coded_in);
  return true;
}

bool ContainerReaderEntry::HadError() const {
  return reader_->HadError();
}

std::string ContainerReaderEntry::GetError() const {
  return reader_->GetError();
}

ContainerReader::ContainerReader(io::InputStream* in)
    : in_(in),
      adaptor_(in),
      coded_in_(&adaptor_),
      total_entry_count_(0u),
      current_entry_count_(0u),
      entry_(this) {
  ::google::protobuf::uint32 magic;
  if (!coded_in_.ReadLittleEndian32(&magic)) {
    std::ostringstream error;
    error << "failed to read magic from input: " << in_->GetError();
    error_ = error.str();
    return;
  }

  if (magic != kContainerFormatMagic) {
    error_ = "magic value doesn't match AAPT";
    return;
  }

  ::google::protobuf::uint32 version;
  if (!coded_in_.ReadLittleEndian32(&version)) {
    std::ostringstream error;
    error << "failed to read version from input: " << in_->GetError();
    error_ = error.str();
    return;
  }

  if (version != kContainerFormatVersion) {
    error_ = StringPrintf("container version is 0x%08x but AAPT expects version 0x%08x", version,
                          kContainerFormatVersion);
    return;
  }

  ::google::protobuf::uint32 total_entry_count;
  if (!coded_in_.ReadLittleEndian32(&total_entry_count)) {
    std::ostringstream error;
    error << "failed to read entry count from input: " << in_->GetError();
    error_ = error.str();
    return;
  }

  total_entry_count_ = total_entry_count;
}

ContainerReaderEntry* ContainerReader::Next() {
  if (current_entry_count_ >= total_entry_count_) {
    return nullptr;
  }
  current_entry_count_++;

  // Ensure the next read is aligned.
  AlignRead(&coded_in_);

  ::google::protobuf::uint32 entry_type;
  if (!coded_in_.ReadLittleEndian32(&entry_type)) {
    std::ostringstream error;
    error << "failed reading entry type from input: " << in_->GetError();
    error_ = error.str();
    return nullptr;
  }

  ::google::protobuf::uint64 entry_length;
  if (!coded_in_.ReadLittleEndian64(&entry_length)) {
    std::ostringstream error;
    error << "failed reading entry length from input: " << in_->GetError();
    error_ = error.str();
    return nullptr;
  }

  if (entry_type == ContainerEntryType::kResFile || entry_type == ContainerEntryType::kResTable) {
    entry_.type_ = static_cast<ContainerEntryType>(entry_type);
  } else {
    error_ = StringPrintf("entry type 0x%08x is invalid", entry_type);
    return nullptr;
  }

  if (entry_length > std::numeric_limits<size_t>::max()) {
    std::ostringstream error;
    error << "entry length " << entry_length << " is too large";
    error_ = error.str();
    return nullptr;
  }

  entry_.length_ = entry_length;
  return &entry_;
}

bool ContainerReader::HadError() const {
  return !error_.empty();
}

std::string ContainerReader::GetError() const {
  return error_;
}

}  // namespace aapt
