//
// Copyright (C) 2021 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 <algorithm>
#include <optional>
#include <vector>

#include "update_engine/common/utils.h"
#include "update_engine/payload_consumer/xor_extent_writer.h"
#include "update_engine/payload_generator/extent_ranges.h"
#include "update_engine/payload_generator/extent_utils.h"
#include "update_engine/update_metadata.pb.h"

namespace chromeos_update_engine {
bool XORExtentWriter::WriteXorCowOp(const uint8_t* bytes,
                                    const size_t size,
                                    const Extent& xor_ext,
                                    const size_t src_offset) {
  xor_block_data.resize(BlockSize() * xor_ext.num_blocks());
  const auto src_block = src_offset / BlockSize();
  ssize_t bytes_read = 0;
  TEST_AND_RETURN_FALSE_ERRNO(utils::PReadAll(source_fd_,
                                              xor_block_data.data(),
                                              xor_block_data.size(),
                                              src_offset,
                                              &bytes_read));
  if (bytes_read != static_cast<ssize_t>(xor_block_data.size())) {
    LOG(ERROR) << "bytes_read: " << bytes_read << ", expected to read "
               << xor_block_data.size() << " at block " << src_block
               << " offset " << src_offset % BlockSize();
    return false;
  }

  std::transform(xor_block_data.cbegin(),
                 xor_block_data.cbegin() + xor_block_data.size(),
                 bytes,
                 xor_block_data.begin(),
                 std::bit_xor<unsigned char>{});
  TEST_AND_RETURN_FALSE(cow_writer_->AddXorBlocks(xor_ext.start_block(),
                                                  xor_block_data.data(),
                                                  xor_block_data.size(),
                                                  src_block,
                                                  src_offset % BlockSize()));
  return true;
}

bool XORExtentWriter::WriteXorExtent(const uint8_t* bytes,
                                     const size_t size,
                                     const Extent& xor_ext,
                                     const CowMergeOperation* merge_op) {
  const auto src_block = merge_op->src_extent().start_block() +
                         xor_ext.start_block() -
                         merge_op->dst_extent().start_block();
  const auto read_end_offset =
      (src_block + xor_ext.num_blocks()) * BlockSize() + merge_op->src_offset();
  const auto is_out_of_bound_read =
      read_end_offset > partition_size_ && partition_size_ != 0;
  const auto oob_bytes =
      is_out_of_bound_read ? read_end_offset - partition_size_ : 0;
  if (is_out_of_bound_read) {
    if (oob_bytes >= BlockSize()) {
      LOG(ERROR) << "XOR op overflowed source partition by more than "
                 << BlockSize() << ", " << xor_ext << ", " << merge_op
                 << ", out of bound bytes: " << oob_bytes
                 << ", partition size: " << partition_size_;
      return false;
    }
    if (oob_bytes > merge_op->src_offset()) {
      LOG(ERROR) << "XOR op overflowed source offset, out of bound bytes: "
                 << oob_bytes << ", source offset: " << merge_op->src_offset();
    }
    Extent non_oob_extent =
        ExtentForRange(xor_ext.start_block(), xor_ext.num_blocks() - 1);
    if (non_oob_extent.num_blocks() > 0) {
      TEST_AND_RETURN_FALSE(
          WriteXorCowOp(bytes,
                        BlockSize() * non_oob_extent.num_blocks(),
                        non_oob_extent,
                        src_block * BlockSize() + merge_op->src_offset()));
    }
    const Extent last_block =
        ExtentForRange(xor_ext.start_block() + xor_ext.num_blocks() - 1, 1);
    TEST_AND_RETURN_FALSE(
        WriteXorCowOp(bytes + (xor_ext.num_blocks() - 1) * BlockSize(),
                      BlockSize(),
                      last_block,
                      (src_block + xor_ext.num_blocks() - 1) * BlockSize()));
    return true;
  }
  TEST_AND_RETURN_FALSE(WriteXorCowOp(
      bytes, size, xor_ext, src_block * BlockSize() + merge_op->src_offset()));
  return true;
}

// Returns true on success.
bool XORExtentWriter::WriteExtent(const void* bytes,
                                  const Extent& extent,
                                  const size_t size) {
  const auto xor_extents = xor_map_.GetIntersectingExtents(extent);
  for (const auto& xor_ext : xor_extents) {
    const auto merge_op_opt = xor_map_.Get(xor_ext);
    if (!merge_op_opt.has_value()) {
      // If a file in the target build contains duplicate blocks, e.g.
      // [120503-120514], [120503-120503], we can end up here. If that's the
      // case then there's no bug, just some annoying edge cases.
      LOG(ERROR)
          << xor_ext
          << " isn't in XOR map but it's returned by GetIntersectingExtents(), "
             "this is either a bug inside GetIntersectingExtents, or some "
             "duplicate blocks are present in target build. OTA extent: "
          << extent;
      return false;
    }

    const auto merge_op = merge_op_opt.value();
    TEST_AND_RETURN_FALSE(merge_op->has_src_extent());
    TEST_AND_RETURN_FALSE(merge_op->has_dst_extent());
    if (!ExtentContains(extent, xor_ext)) {
      LOG(ERROR) << "CowXor merge op extent should be completely inside "
                    "InstallOp's extent. merge op extent: "
                 << xor_ext << " InstallOp extent: " << extent;
      return false;
    }
    if (!ExtentContains(merge_op->dst_extent(), xor_ext)) {
      LOG(ERROR) << "CowXor op extent should be completely inside "
                    "xor_map's extent. merge op extent: "
                 << xor_ext << " xor_map extent: " << merge_op->dst_extent();
      return false;
    }
    const auto i = xor_ext.start_block() - extent.start_block();
    const auto dst_block_data =
        static_cast<const unsigned char*>(bytes) + i * BlockSize();
    if (!WriteXorExtent(dst_block_data,
                        xor_ext.num_blocks() * BlockSize(),
                        xor_ext,
                        merge_op)) {
      LOG(ERROR) << "Failed to write XOR extent " << xor_ext;
      return false;
    }
  }
  const auto replace_extents = xor_map_.GetNonIntersectingExtents(extent);
  return WriteReplaceExtents(replace_extents, extent, bytes, size);
}

bool XORExtentWriter::WriteReplaceExtents(
    const std::vector<Extent>& replace_extents,
    const Extent& extent,
    const void* bytes,
    size_t size) {
  const uint64_t new_block_start = extent.start_block();
  for (const auto& ext : replace_extents) {
    if (ext.start_block() + ext.num_blocks() >
        extent.start_block() + extent.num_blocks()) {
      LOG(ERROR) << "CowReplace merge op extent should be completely inside "
                    "InstallOp's extent. merge op extent: "
                 << ext << " InstallOp extent: " << extent;
      return false;
    }
    const auto i = ext.start_block() - new_block_start;
    const auto dst_block_data =
        static_cast<const unsigned char*>(bytes) + i * BlockSize();
    TEST_AND_RETURN_FALSE(cow_writer_->AddRawBlocks(
        ext.start_block(), dst_block_data, ext.num_blocks() * BlockSize()));
  }
  return true;
}

}  // namespace chromeos_update_engine