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

#include "base/trace_event/trace_arguments.h"

#include <inttypes.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>

#include <cmath>
#include <ostream>

#include "base/check_op.h"
#include "base/json/string_escape.h"
#include "base/memory/raw_ptr.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"

namespace base {
namespace trace_event {

namespace {

size_t GetAllocLength(const char* str) {
  return str ? strlen(str) + 1 : 0;
}

// Copies |*member| into |*buffer|, sets |*member| to point to this new
// location, and then advances |*buffer| by the amount written.
void CopyTraceEventParameter(char** buffer,
                             const char** member,
                             const char* end) {
  if (*member) {
    DCHECK_GE(end, *buffer);
    size_t written =
        strlcpy(*buffer, *member, static_cast<size_t>(end - *buffer)) + 1;
    DCHECK_LE(static_cast<ptrdiff_t>(written), end - *buffer);
    *member = *buffer;
    *buffer += written;
  }
}

// Append |val| as a JSON output value to |*out|.
void AppendDouble(double val, bool as_json, std::string* out) {
  // FIXME: base/json/json_writer.cc is using the same code,
  //        should be made into a common method.
  std::string real;
  if (std::isfinite(val)) {
    real = NumberToString(val);
    // Ensure that the number has a .0 if there's no decimal or 'e'.  This
    // makes sure that when we read the JSON back, it's interpreted as a
    // real rather than an int.
    if (real.find('.') == std::string::npos &&
        real.find('e') == std::string::npos &&
        real.find('E') == std::string::npos) {
      real.append(".0");
    }
    // The JSON spec requires that non-integer values in the range (-1,1)
    // have a zero before the decimal point - ".52" is not valid, "0.52" is.
    if (real[0] == '.') {
      real.insert(0, "0");
    } else if (real.length() > 1 && real[0] == '-' && real[1] == '.') {
      // "-.1" bad "-0.1" good
      real.insert(1, "0");
    }
  } else if (std::isnan(val)) {
    // The JSON spec doesn't allow NaN and Infinity (since these are
    // objects in EcmaScript).  Use strings instead.
    real = as_json ? "\"NaN\"" : "NaN";
  } else if (val < 0) {
    real = as_json ? "\"-Infinity\"" : "-Infinity";
  } else {
    real = as_json ? "\"Infinity\"" : "Infinity";
  }
  StringAppendF(out, "%s", real.c_str());
}

const char* TypeToString(unsigned char arg_type) {
  switch (arg_type) {
    case TRACE_VALUE_TYPE_INT:
      return "int";
    case TRACE_VALUE_TYPE_UINT:
      return "uint";
    case TRACE_VALUE_TYPE_DOUBLE:
      return "double";
    case TRACE_VALUE_TYPE_BOOL:
      return "bool";
    case TRACE_VALUE_TYPE_POINTER:
      return "pointer";
    case TRACE_VALUE_TYPE_STRING:
      return "string";
    case TRACE_VALUE_TYPE_COPY_STRING:
      return "copy_string";
    case TRACE_VALUE_TYPE_CONVERTABLE:
      return "convertable";
    default:
      NOTREACHED();
      return "UNKNOWN_TYPE";
  }
}

void AppendValueDebugString(const TraceArguments& args,
                            size_t idx,
                            std::string* out) {
  *out += (args.names()[idx] ? args.names()[idx] : "NULL_NAME");
  *out += "=";
  *out += TypeToString(args.types()[idx]);
  *out += "(";
  args.values()[idx].AppendAsJSON(args.types()[idx], out);
  *out += ")";
}

#if BUILDFLAG(USE_PERFETTO_CLIENT_LIBRARY)
class PerfettoProtoAppender : public ConvertableToTraceFormat::ProtoAppender {
 public:
  explicit PerfettoProtoAppender(
      perfetto::protos::pbzero::DebugAnnotation* proto)
      : annotation_proto_(proto) {}
  ~PerfettoProtoAppender() override = default;

  void AddBuffer(uint8_t* begin, uint8_t* end) override {
    ranges_.emplace_back();
    ranges_.back().begin = begin;
    ranges_.back().end = end;
  }

  size_t Finalize(uint32_t field_id) override {
    return annotation_proto_->AppendScatteredBytes(field_id, ranges_.data(),
                                                   ranges_.size());
  }

 private:
  std::vector<protozero::ContiguousMemoryRange> ranges_;
  raw_ptr<perfetto::protos::pbzero::DebugAnnotation> annotation_proto_;
};
#endif  // BUILDFLAG(USE_PERFETTO_CLIENT_LIBRARY)

}  // namespace

void StringStorage::Reset(size_t alloc_size) {
  if (!alloc_size) {
    if (data_)
      ::free(data_);
    data_ = nullptr;
  } else if (!data_ || alloc_size != data_->size) {
    data_ = static_cast<Data*>(::realloc(data_, sizeof(size_t) + alloc_size));
    data_->size = alloc_size;
  }
}

bool StringStorage::Contains(const TraceArguments& args) const {
  for (size_t n = 0; n < args.size(); ++n) {
    if (args.types()[n] == TRACE_VALUE_TYPE_COPY_STRING &&
        !Contains(args.values()[n].as_string)) {
      return false;
    }
  }
  return true;
}

static_assert(
    std::is_trivial_v<TraceValue> && std::is_standard_layout_v<TraceValue>,
    "TraceValue must be plain-old-data type for performance reasons!");

void TraceValue::AppendAsJSON(unsigned char type, std::string* out) const {
  Append(type, true, out);
}

void TraceValue::AppendAsString(unsigned char type, std::string* out) const {
  Append(type, false, out);
}

void TraceValue::Append(unsigned char type,
                        bool as_json,
                        std::string* out) const {
  switch (type) {
    case TRACE_VALUE_TYPE_BOOL:
      *out += this->as_bool ? "true" : "false";
      break;
    case TRACE_VALUE_TYPE_UINT:
      StringAppendF(out, "%" PRIu64, static_cast<uint64_t>(this->as_uint));
      break;
    case TRACE_VALUE_TYPE_INT:
      StringAppendF(out, "%" PRId64, static_cast<int64_t>(this->as_int));
      break;
    case TRACE_VALUE_TYPE_DOUBLE:
      AppendDouble(this->as_double, as_json, out);
      break;
    case TRACE_VALUE_TYPE_POINTER: {
      // JSON only supports double and int numbers.
      // So as not to lose bits from a 64-bit pointer, output as a hex string.
      // For consistency, do the same for non-JSON strings, but without the
      // surrounding quotes.
      const std::string value = StringPrintf(
          "0x%" PRIx64,
          static_cast<uint64_t>(reinterpret_cast<uintptr_t>(this->as_pointer)));
      *out += as_json ? StrCat({"\"", value, "\""}) : value;
    } break;
    case TRACE_VALUE_TYPE_STRING:
    case TRACE_VALUE_TYPE_COPY_STRING:
      if (as_json)
        EscapeJSONString(this->as_string ? this->as_string : "NULL", true, out);
      else
        *out += this->as_string ? this->as_string : "NULL";
      break;
    case TRACE_VALUE_TYPE_CONVERTABLE:
      this->as_convertable->AppendAsTraceFormat(out);
      break;
    case TRACE_VALUE_TYPE_PROTO:
      DCHECK(as_json);
      // Typed protobuf arguments aren't representable in JSON.
      *out += "\"Unsupported (crbug.com/1225176)\"";
      break;
    default:
      NOTREACHED() << "Don't know how to print this value";
      break;
  }
}

TraceArguments& TraceArguments::operator=(TraceArguments&& other) noexcept {
  if (this != &other) {
    this->~TraceArguments();
    new (this) TraceArguments(std::move(other));
  }
  return *this;
}

TraceArguments::TraceArguments(int num_args,
                               const char* const* arg_names,
                               const unsigned char* arg_types,
                               const unsigned long long* arg_values) {
  if (num_args > static_cast<int>(kMaxSize))
    num_args = static_cast<int>(kMaxSize);

  size_ = static_cast<unsigned char>(num_args);
  for (size_t n = 0; n < size_; ++n) {
    types_[n] = arg_types[n];
    names_[n] = arg_names[n];
    values_[n].as_uint = arg_values[n];
  }
}

void TraceArguments::Reset() {
  for (size_t n = 0; n < size_; ++n) {
    if (types_[n] == TRACE_VALUE_TYPE_CONVERTABLE)
      delete values_[n].as_convertable;
  }
  size_ = 0;
}

void TraceArguments::CopyStringsTo(StringStorage* storage,
                                   bool copy_all_strings,
                                   const char** extra_string1,
                                   const char** extra_string2) {
  // First, compute total allocation size.
  size_t alloc_size = 0;

  if (copy_all_strings) {
    alloc_size +=
        GetAllocLength(*extra_string1) + GetAllocLength(*extra_string2);
    for (size_t n = 0; n < size_; ++n)
      alloc_size += GetAllocLength(names_[n]);
  }
  for (size_t n = 0; n < size_; ++n) {
    if (copy_all_strings && types_[n] == TRACE_VALUE_TYPE_STRING)
      types_[n] = TRACE_VALUE_TYPE_COPY_STRING;
    if (types_[n] == TRACE_VALUE_TYPE_COPY_STRING)
      alloc_size += GetAllocLength(values_[n].as_string);
  }

  if (alloc_size) {
    storage->Reset(alloc_size);
    char* ptr = storage->data();
    const char* end = ptr + alloc_size;
    if (copy_all_strings) {
      CopyTraceEventParameter(&ptr, extra_string1, end);
      CopyTraceEventParameter(&ptr, extra_string2, end);
      for (size_t n = 0; n < size_; ++n)
        CopyTraceEventParameter(&ptr, &names_[n], end);
    }
    for (size_t n = 0; n < size_; ++n) {
      if (types_[n] == TRACE_VALUE_TYPE_COPY_STRING)
        CopyTraceEventParameter(&ptr, &values_[n].as_string, end);
    }
#if DCHECK_IS_ON()
    DCHECK_EQ(end, ptr) << "Overrun by " << ptr - end;
    if (copy_all_strings) {
      if (extra_string1 && *extra_string1)
        DCHECK(storage->Contains(*extra_string1));
      if (extra_string2 && *extra_string2)
        DCHECK(storage->Contains(*extra_string2));
      for (size_t n = 0; n < size_; ++n)
        DCHECK(storage->Contains(names_[n]));
    }
    for (size_t n = 0; n < size_; ++n) {
      if (types_[n] == TRACE_VALUE_TYPE_COPY_STRING)
        DCHECK(storage->Contains(values_[n].as_string));
    }
#endif  // DCHECK_IS_ON()
  } else {
    storage->Reset();
  }
}

void TraceArguments::AppendDebugString(std::string* out) {
  *out += "TraceArguments(";
  for (size_t n = 0; n < size_; ++n) {
    if (n > 0)
      *out += ", ";
    AppendValueDebugString(*this, n, out);
  }
  *out += ")";
}

#if BUILDFLAG(USE_PERFETTO_CLIENT_LIBRARY)
void ConvertableToTraceFormat::Add(
    perfetto::protos::pbzero::DebugAnnotation* annotation) const {
  PerfettoProtoAppender proto_appender(annotation);
  if (AppendToProto(&proto_appender)) {
    return;
  }

  std::string json;
  AppendAsTraceFormat(&json);
  annotation->set_legacy_json_value(json);
}
#endif  // BUILDFLAG(USE_PERFETTO_CLIENT_LIBRARY)

}  // namespace trace_event
}  // namespace base
