// Copyright (c) 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// wire_serialization.h -- absl::StrCat()-like interface for QUICHE wire format.
//
// When serializing a data structure, there are two common approaches:
//   (1) Allocate into a dynamically sized buffer and incur the costs of memory
//       allocations.
//   (2) Precompute the length of the structure, allocate a buffer of the
//       exact required size and then write into the said buffer.
//  QUICHE generally takes the second approach, but as a result, a lot of
//  serialization code is written twice. This API avoids this issue by letting
//  the caller declaratively describe the wire format; the description provided
//  is used both for the size computation and for the serialization.
//
// Consider the following struct in RFC 9000 language:
//   Test Struct {
//     Magic Value (32),
//     Some Number (i),
//     [Optional Number (i)],
//     Magical String Length (i),
//     Magical String (..),
//   }
//
// Using the functions in this header, it can be serialized as follows:
//   absl::StatusOr<quiche::QuicheBuffer> test_struct = SerializeIntoBuffer(
//     WireUint32(magic_value),
//     WireVarInt62(some_number),
//     WireOptional<WireVarint62>(optional_number),
//     WireStringWithVarInt62Length(magical_string)
//   );
//
// This header provides three main functions with fairly self-explanatory names:
//  - size_t ComputeLengthOnWire(d1, d2, ... dN)
//  - absl::Status SerializeIntoWriter(writer, d1, d2, ... dN)
//  - absl::StatusOr<QuicheBuffer> SerializeIntoBuffer(allocator, d1, ... dN)
//
// It is possible to define a custom serializer for individual structs. Those
// would normally look like this:
//
//     struct AwesomeStruct { ... }
//     class WireAwesomeStruct {
//      public:
//       using DataType = AwesomeStruct;
//       WireAwesomeStruct(const AwesomeStruct& awesome) : awesome_(awesome) {}
//       size_t GetLengthOnWire() { ... }
//       absl::Status SerializeIntoWriter(QuicheDataWriter& writer) { ... }
//     };
//
// See the unit test for the full version of the example above.

#ifndef QUICHE_COMMON_WIRE_SERIALIZATION_H_
#define QUICHE_COMMON_WIRE_SERIALIZATION_H_

#include <cstddef>
#include <cstdint>
#include <optional>
#include <tuple>
#include <type_traits>
#include <utility>

#include "absl/base/attributes.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/string_view.h"
#include "quiche/common/platform/api/quiche_logging.h"
#include "quiche/common/quiche_buffer_allocator.h"
#include "quiche/common/quiche_data_writer.h"
#include "quiche/common/quiche_status_utils.h"

namespace quiche {

// T::SerializeIntoWriter() is allowed to return both a bool and an
// absl::Status.  There are two reasons for that:
//   1. Most QuicheDataWriter methods return a bool.
//   2. While cheap, absl::Status has a non-trivial destructor and thus is not
//      as free as a bool is.
// To accomodate this, SerializeIntoWriterStatus<T> provides a way to deduce
// what is the status type returned by the SerializeIntoWriter method.
template <typename T>
class QUICHE_NO_EXPORT SerializeIntoWriterStatus {
 public:
  static_assert(std::is_trivially_copyable_v<T> && sizeof(T) <= 32,
                "The types passed into SerializeInto() APIs are passed by "
                "value; if your type has non-trivial copy costs, it should be "
                "wrapped into a type that carries a pointer");

  using Type = decltype(std::declval<T>().SerializeIntoWriter(
      std::declval<QuicheDataWriter&>()));
  static constexpr bool kIsBool = std::is_same_v<Type, bool>;
  static constexpr bool kIsStatus = std::is_same_v<Type, absl::Status>;
  static_assert(
      kIsBool || kIsStatus,
      "SerializeIntoWriter() has to return either a bool or an absl::Status");

  static ABSL_ATTRIBUTE_ALWAYS_INLINE Type OkValue() {
    if constexpr (kIsStatus) {
      return absl::OkStatus();
    } else {
      return true;
    }
  }
};

inline ABSL_ATTRIBUTE_ALWAYS_INLINE bool IsWriterStatusOk(bool status) {
  return status;
}
inline ABSL_ATTRIBUTE_ALWAYS_INLINE bool IsWriterStatusOk(
    const absl::Status& status) {
  return status.ok();
}

// ------------------- WireType() wrapper definitions -------------------

// Base class for WireUint8/16/32/64.
template <typename T>
class QUICHE_EXPORT WireFixedSizeIntBase {
 public:
  using DataType = T;
  static_assert(std::is_integral_v<DataType>,
                "WireFixedSizeIntBase is only usable with integral types");

  explicit WireFixedSizeIntBase(T value) { value_ = value; }
  size_t GetLengthOnWire() const { return sizeof(T); }
  T value() const { return value_; }

 private:
  T value_;
};

// Fixed-size integer fields.  Correspond to (8), (16), (32) and (64) fields in
// RFC 9000 language.
class QUICHE_EXPORT WireUint8 : public WireFixedSizeIntBase<uint8_t> {
 public:
  using WireFixedSizeIntBase::WireFixedSizeIntBase;
  bool SerializeIntoWriter(QuicheDataWriter& writer) const {
    return writer.WriteUInt8(value());
  }
};
class QUICHE_EXPORT WireUint16 : public WireFixedSizeIntBase<uint16_t> {
 public:
  using WireFixedSizeIntBase::WireFixedSizeIntBase;
  bool SerializeIntoWriter(QuicheDataWriter& writer) const {
    return writer.WriteUInt16(value());
  }
};
class QUICHE_EXPORT WireUint32 : public WireFixedSizeIntBase<uint32_t> {
 public:
  using WireFixedSizeIntBase::WireFixedSizeIntBase;
  bool SerializeIntoWriter(QuicheDataWriter& writer) const {
    return writer.WriteUInt32(value());
  }
};
class QUICHE_EXPORT WireUint64 : public WireFixedSizeIntBase<uint64_t> {
 public:
  using WireFixedSizeIntBase::WireFixedSizeIntBase;
  bool SerializeIntoWriter(QuicheDataWriter& writer) const {
    return writer.WriteUInt64(value());
  }
};

// Represents a 62-bit variable-length non-negative integer.  Those are
// described in the Section 16 of RFC 9000, and are denoted as (i) in type
// descriptions.
class QUICHE_EXPORT WireVarInt62 {
 public:
  using DataType = uint64_t;

  explicit WireVarInt62(uint64_t value) { value_ = value; }
  // Convenience wrapper. This is safe, since it is clear from the context that
  // the enum is being treated as an integer.
  template <typename T>
  explicit WireVarInt62(T value) {
    static_assert(std::is_enum_v<T> || std::is_convertible_v<T, uint64_t>);
    value_ = static_cast<uint64_t>(value);
  }

  size_t GetLengthOnWire() const {
    return QuicheDataWriter::GetVarInt62Len(value_);
  }
  bool SerializeIntoWriter(QuicheDataWriter& writer) const {
    return writer.WriteVarInt62(value_);
  }

 private:
  uint64_t value_;
};

// Represents unframed raw string.
class QUICHE_EXPORT WireBytes {
 public:
  using DataType = absl::string_view;

  explicit WireBytes(absl::string_view value) { value_ = value; }
  size_t GetLengthOnWire() { return value_.size(); }
  bool SerializeIntoWriter(QuicheDataWriter& writer) {
    return writer.WriteStringPiece(value_);
  }

 private:
  absl::string_view value_;
};

// Represents a string where another wire type is used as a length prefix.
template <class LengthWireType>
class QUICHE_EXPORT WireStringWithLengthPrefix {
 public:
  using DataType = absl::string_view;

  explicit WireStringWithLengthPrefix(absl::string_view value) {
    value_ = value;
  }
  size_t GetLengthOnWire() {
    return LengthWireType(value_.size()).GetLengthOnWire() + value_.size();
  }
  absl::Status SerializeIntoWriter(QuicheDataWriter& writer) {
    if (!LengthWireType(value_.size()).SerializeIntoWriter(writer)) {
      return absl::InternalError("Failed to serialize the length prefix");
    }
    if (!writer.WriteStringPiece(value_)) {
      return absl::InternalError("Failed to serialize the string proper");
    }
    return absl::OkStatus();
  }

 private:
  absl::string_view value_;
};

// Represents varint62-prefixed strings.
using WireStringWithVarInt62Length = WireStringWithLengthPrefix<WireVarInt62>;

// Allows std::optional to be used with this API. For instance, if the spec
// defines
//   [Context ID (i)]
// and the value is stored as std::optional<uint64> context_id, this can be
// recorded as
//   WireOptional<WireVarInt62>(context_id)
// When optional is absent, nothing is written onto the wire.
template <typename WireType, typename InnerType = typename WireType::DataType>
class QUICHE_EXPORT WireOptional {
 public:
  using DataType = std::optional<InnerType>;
  using Status = SerializeIntoWriterStatus<WireType>;

  explicit WireOptional(DataType value) { value_ = value; }
  size_t GetLengthOnWire() const {
    return value_.has_value() ? WireType(*value_).GetLengthOnWire() : 0;
  }
  typename Status::Type SerializeIntoWriter(QuicheDataWriter& writer) const {
    if (value_.has_value()) {
      return WireType(*value_).SerializeIntoWriter(writer);
    }
    return Status::OkValue();
  }

 private:
  DataType value_;
};

// Allows multiple entries of the same type to be serialized in a single call.
template <typename WireType,
          typename SpanElementType = typename WireType::DataType>
class QUICHE_EXPORT WireSpan {
 public:
  using DataType = absl::Span<const SpanElementType>;

  explicit WireSpan(DataType value) { value_ = value; }
  size_t GetLengthOnWire() const {
    size_t total = 0;
    for (const SpanElementType& value : value_) {
      total += WireType(value).GetLengthOnWire();
    }
    return total;
  }
  absl::Status SerializeIntoWriter(QuicheDataWriter& writer) const {
    for (size_t i = 0; i < value_.size(); i++) {
      // `status` here can be either a bool or an absl::Status.
      auto status = WireType(value_[i]).SerializeIntoWriter(writer);
      if (IsWriterStatusOk(status)) {
        continue;
      }
      if constexpr (SerializeIntoWriterStatus<WireType>::kIsStatus) {
        return AppendToStatus(std::move(status),
                              " while serializing the value #", i);
      } else {
        return absl::InternalError(
            absl::StrCat("Failed to serialize vector value #", i));
      }
    }
    return absl::OkStatus();
  }

 private:
  DataType value_;
};

// ------------------- Top-level serialization API -------------------

namespace wire_serialization_internal {
template <typename T>
auto SerializeIntoWriterWrapper(QuicheDataWriter& writer, int argno, T data) {
#if defined(NDEBUG)
  (void)argno;
  (void)data;
  return data.SerializeIntoWriter(writer);
#else
  // When running in the debug build, we check that the length reported by
  // GetLengthOnWire() matches what is actually being written onto the wire.
  // While any mismatch will most likely lead to an error further down the line,
  // this simplifies the debugging process.
  const size_t initial_offset = writer.length();
  const size_t expected_size = data.GetLengthOnWire();
  auto result = data.SerializeIntoWriter(writer);
  const size_t final_offset = writer.length();
  if (IsWriterStatusOk(result)) {
    QUICHE_DCHECK_EQ(initial_offset + expected_size, final_offset)
        << "while serializing field #" << argno;
  }
  return result;
#endif
}

template <typename T>
std::enable_if_t<SerializeIntoWriterStatus<T>::kIsBool, absl::Status>
SerializeIntoWriterCore(QuicheDataWriter& writer, int argno, T data) {
  const bool success = SerializeIntoWriterWrapper(writer, argno, data);
  if (!success) {
    return absl::InternalError(
        absl::StrCat("Failed to serialize field #", argno));
  }
  return absl::OkStatus();
}

template <typename T>
std::enable_if_t<SerializeIntoWriterStatus<T>::kIsStatus, absl::Status>
SerializeIntoWriterCore(QuicheDataWriter& writer, int argno, T data) {
  return AppendToStatus(SerializeIntoWriterWrapper(writer, argno, data),
                        " while serializing field #", argno);
}

template <typename T1, typename... Ts>
absl::Status SerializeIntoWriterCore(QuicheDataWriter& writer, int argno,
                                     T1 data1, Ts... rest) {
  QUICHE_RETURN_IF_ERROR(SerializeIntoWriterCore(writer, argno, data1));
  return SerializeIntoWriterCore(writer, argno + 1, rest...);
}

inline absl::Status SerializeIntoWriterCore(QuicheDataWriter&, int) {
  return absl::OkStatus();
}
}  // namespace wire_serialization_internal

// SerializeIntoWriter(writer, d1, d2, ... dN) serializes all of supplied data
// into the writer |writer|.  True is returned on success, and false is returned
// if serialization fails (typically because the writer ran out of buffer). This
// is conceptually similar to absl::StrAppend().
template <typename... Ts>
absl::Status SerializeIntoWriter(QuicheDataWriter& writer, Ts... data) {
  return wire_serialization_internal::SerializeIntoWriterCore(
      writer, /*argno=*/0, data...);
}

// ComputeLengthOnWire(writer, d1, d2, ... dN) calculates the number of bytes
// necessary to serialize the supplied data.
template <typename T>
size_t ComputeLengthOnWire(T data) {
  return data.GetLengthOnWire();
}
template <typename T1, typename... Ts>
size_t ComputeLengthOnWire(T1 data1, Ts... rest) {
  return data1.GetLengthOnWire() + ComputeLengthOnWire(rest...);
}
inline size_t ComputeLengthOnWire() { return 0; }

// SerializeIntoBuffer(allocator, d1, d2, ... dN) computes the length required
// to store the supplied data, allocates the buffer of appropriate size using
// |allocator|, and serializes the result into it.  In a rare event that the
// serialization fails (e.g. due to invalid varint62 value), an empty buffer is
// returned.
template <typename... Ts>
absl::StatusOr<QuicheBuffer> SerializeIntoBuffer(
    QuicheBufferAllocator* allocator, Ts... data) {
  size_t buffer_size = ComputeLengthOnWire(data...);
  if (buffer_size == 0) {
    return QuicheBuffer();
  }

  QuicheBuffer buffer(allocator, buffer_size);
  QuicheDataWriter writer(buffer.size(), buffer.data());
  QUICHE_RETURN_IF_ERROR(SerializeIntoWriter(writer, data...));
  if (writer.remaining() != 0) {
    return absl::InternalError(absl::StrCat(
        "Excess ", writer.remaining(), " bytes allocated while serializing"));
  }
  return buffer;
}

// SerializeIntoBuffer() that returns std::string instead of QuicheBuffer.
template <typename... Ts>
absl::StatusOr<std::string> SerializeIntoString(Ts... data) {
  size_t buffer_size = ComputeLengthOnWire(data...);
  if (buffer_size == 0) {
    return std::string();
  }

  std::string buffer;
  buffer.resize(buffer_size);
  QuicheDataWriter writer(buffer.size(), buffer.data());
  QUICHE_RETURN_IF_ERROR(SerializeIntoWriter(writer, data...));
  if (writer.remaining() != 0) {
    return absl::InternalError(absl::StrCat(
        "Excess ", writer.remaining(), " bytes allocated while serializing"));
  }
  return buffer;
}

}  // namespace quiche

#endif  // QUICHE_COMMON_WIRE_SERIALIZATION_H_
