// Copyright 2006 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/sessions/core/session_command.h"

#include <algorithm>
#include <limits>
#include <memory>

#include "base/compiler_specific.h"
#include "base/containers/span.h"
#include "base/logging.h"
#include "base/numerics/byte_conversions.h"
#include "base/pickle.h"
#include "components/os_crypt/async/common/encryptor.h"

namespace sessions {

// The encrypted size field is a uint32_t because the encrypted contents can be
// larger than kMaxContentSize.
using encrypted_size_type = uint32_t;

SessionCommand::SessionCommand(id_type id, size_type size)
    : id_(id), contents_(size, 0) {
  // Note that it is possible for size to be greater than kMaxContentSize.
  // This is allowed for historical reasons, the contents will be truncated when
  // Serialize() is called.
}

SessionCommand::SessionCommand(id_type id, const base::Pickle& pickle)
    : id_(id), contents_(pickle.size(), 0) {
  DCHECK(pickle.size() < kMaxContentSize);
  contents().copy_from(pickle);
}

std::unique_ptr<SessionCommand> SessionCommand::Clone() const {
  auto clone = std::make_unique<SessionCommand>(
      id(), static_cast<size_type>(contents().size()));
  clone->contents().copy_from(contents());
  return clone;
}

bool SessionCommand::operator==(const SessionCommand& command) const {
  return id_ == command.id_ && contents_ == command.contents_;
}

bool SessionCommand::GetContents(base::span<uint8_t> dest) const {
  if (contents_.size() != dest.size()) {
    return false;
  }
  dest.copy_from(contents());
  return true;
}

base::PickleIterator SessionCommand::ContentsAsPickle() const {
  return base::PickleIterator::WithData(contents());
}
std::vector<uint8_t> SessionCommand::Serialize(
    os_crypt_async::Encryptor* encryptor) const {
  if (encryptor) {
    return SerializeWithEncryption(*encryptor);
  } else {
    return SerializeAsCleartext();
  }
}

std::vector<uint8_t> SessionCommand::SerializeAsCleartext() const {
  if (contents().size() > kMaxContentSize) {
    VLOG(2) << "SessionCommand::Serialize: contents_size " << contents().size()
            << " is greater than kMaxContentSize " << kMaxContentSize
            << " and will be truncated.";
  }
  const size_type contents_size = std::min(contents().size(), kMaxContentSize);
  // Note that total_size can be greater that UINT16_MAX, so we use size_t
  // instead of size_type.
  const size_type size_field_value = sizeof(id_type) + contents_size;
  const size_t total_size = sizeof(size_type) + size_field_value;
  std::vector<uint8_t> result(total_size);
  base::span<uint8_t> remaining = base::span(result);
  remaining.take_first(sizeof(size_type))
      .copy_from(base::U16ToNativeEndian(size_field_value));
  remaining.take_first(sizeof(id_type))
      .copy_from(base::byte_span_from_ref(id()));
  // This is where truncation of contents_ can occur.
  remaining.copy_from(contents().first(contents_size));
  return result;
}

std::vector<uint8_t> SessionCommand::SerializeWithEncryption(
    const os_crypt_async::Encryptor& encryptor) const {
  // Emulate the behavior of SerializeAsCleartext() by truncating the contents
  // to kMaxContentSize.
  const size_type contents_size = std::min(contents().size(), kMaxContentSize);
  std::string payload;
  payload.reserve(sizeof(id_type) + contents_size);
  payload.push_back(static_cast<char>(id()));
  const base::span<const uint8_t> contents_span =
      contents().first(contents_size);
  payload.append(reinterpret_cast<const char*>(contents_span.data()),
                 contents_span.size());

  std::optional<std::vector<uint8_t>> encrypted =
      encryptor.EncryptString(payload);
  if (!encrypted ||
      encrypted->size() > std::numeric_limits<encrypted_size_type>::max()) {
    return std::vector<uint8_t>();
  }

  const size_t total_size = sizeof(encrypted_size_type) + encrypted->size();
  std::vector<uint8_t> result(total_size);
  base::span<uint8_t> remaining = base::span(result);
  remaining.take_first<sizeof(encrypted_size_type)>().copy_from(
      base::U32ToNativeEndian(
          static_cast<encrypted_size_type>(encrypted->size())));
  remaining.copy_from(*encrypted);

  return result;
}

std::optional<size_t> SessionCommand::GetSerializedSize(
    base::span<const uint8_t> data,
    bool encrypted) {
  size_t sizeof_size_field =
      encrypted ? sizeof(encrypted_size_type) : sizeof(size_type);
  if (data.size() < sizeof_size_field) {
    // If there's just one byte of data, then it's ignored and not an error.
    return std::nullopt;
  }
  size_t size_field_value =
      encrypted
          ? base::U32FromNativeEndian(data.first<sizeof(encrypted_size_type)>())
          : base::U16FromNativeEndian(data.first<sizeof(size_type)>());
  return sizeof_size_field + size_field_value;
}

std::unique_ptr<SessionCommand> SessionCommand::Deserialize(
    base::span<const uint8_t> data,
    os_crypt_async::Encryptor* encryptor) {
  if (encryptor) {
    return DeserializeEncrypted(data, *encryptor);
  } else {
    return DeserializeCleartext(data);
  }
}

std::unique_ptr<SessionCommand> SessionCommand::DeserializeCleartext(
    base::span<const uint8_t> data) {
  if (data.size() < sizeof(size_type)) {
    VLOG(2) << "SessionCommand::Deserialize: data.size() " << data.size()
            << " is less than sizeof(size_type) " << sizeof(size_type);
    return nullptr;
  }
  base::span<const uint8_t> remaining = data;

  // Parse the size field.
  const size_type size_field_value =
      base::U16FromNativeEndian(remaining.take_first<sizeof(size_type)>());
  if (remaining.size() < size_field_value) {
    VLOG(2) << "SessionCommand::Deserialize: remaining.size() "
            << remaining.size() << " is less than size_field_value "
            << size_field_value;
    return nullptr;
  }
  if (size_field_value < sizeof(id_type)) {
    VLOG(2) << "SessionCommand::Deserialize: size_field_value "
            << size_field_value << " is less than sizeof(id_type) "
            << sizeof(id_type);
    return nullptr;
  }

  // Parse the id field.
  const id_type command_id = remaining.take_first<sizeof(id_type)>()[0];

  // Parse the contents field.
  const size_type content_size = size_field_value - sizeof(id_type);
  std::unique_ptr<sessions::SessionCommand> command =
      std::make_unique<sessions::SessionCommand>(
          command_id, static_cast<size_type>(content_size));
  if (content_size > 0) {
    command->contents().copy_from(remaining.take_first(content_size));
  }
  return command;
}

std::unique_ptr<SessionCommand> SessionCommand::DeserializeEncrypted(
    base::span<const uint8_t> data,
    const os_crypt_async::Encryptor& encryptor) {
  if (data.size() < sizeof(encrypted_size_type)) {
    VLOG(2) << "SessionCommand::DeserializeEncrypted: data.size() "
            << data.size() << " is less than sizeof(encrypted_size_type) "
            << sizeof(encrypted_size_type);
    return nullptr;
  }
  base::span<const uint8_t> remaining = data;

  const encrypted_size_type size_field_value = base::U32FromNativeEndian(
      remaining.take_first<sizeof(encrypted_size_type)>());

  if (remaining.size() < size_field_value) {
    VLOG(2) << "SessionCommand::DeserializeEncrypted: remaining.size() "
            << remaining.size() << " is less than size_field_value "
            << size_field_value;
    return nullptr;
  }

  std::optional<std::string> decrypted =
      encryptor.DecryptData(remaining.take_first(size_field_value));

  if (!decrypted) {
    VLOG(2) << "SessionCommand::DeserializeEncrypted: Decryption failed.";
    return nullptr;
  }

  if (decrypted->size() < sizeof(id_type)) {
    VLOG(2) << "SessionCommand::DeserializeEncrypted: decrypted size "
            << decrypted->size() << " is less than sizeof(id_type) "
            << sizeof(id_type);
    return nullptr;
  }

  const id_type command_id = static_cast<id_type>(decrypted->front());
  const size_t content_size = decrypted->size() - sizeof(id_type);

  if (content_size > std::numeric_limits<size_type>::max()) {
    VLOG(2) << "SessionCommand::DeserializeEncrypted: content_size "
            << content_size << " is greater than max size_type";
    return nullptr;
  }

  std::unique_ptr<sessions::SessionCommand> command =
      std::make_unique<sessions::SessionCommand>(
          command_id, static_cast<size_type>(content_size));
  if (content_size > 0) {
    command->contents().copy_from(
        base::as_byte_span(*decrypted).subspan(sizeof(id_type)));
  }
  return command;
}

}  // namespace sessions
