/** @file

  Reference decoder for the self-describing v3 binary log format.

  @section license License

  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
  distributed with this work for additional information
  regarding copyright ownership.  The ASF licenses this file
  to you 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 "LogEntryJson.h"

#include "proxy/logging/LogBuffer.h"
#include "proxy/logging/LogAccess.h"
#include "proxy/logging/LogField.h"

// Deliberately not including the global field table (proxy/logging/Log.h):
// decoding must depend only on the segment's schema (see LogEntryJson.h).

#include "tscore/ink_inet.h"
#include "tscore/ink_align.h"

#include <cinttypes>
#include <cstdint>
#include <cstdio>
#include <cstring>

namespace
{
// fmt_fieldlist symbols are comma-separated (e.g. "chi,cqu,pssc"); tolerate
// spaces too.
bool
is_field_sep(char c)
{
  return c == ',' || c == ' ';
}
} // namespace

int
log_entry_to_json(LogEntryHeader *entry, LogBufferHeader *header, char *buf, int buf_len)
{
  if (entry == nullptr || header == nullptr || buf == nullptr || buf_len <= 0) {
    return -1;
  }

  // [seg_start, seg_end) is the only readable region (byte_count == segment size).
  char *seg_start = reinterpret_cast<char *>(header);
  char *seg_end   = seg_start + header->byte_count;

  // v3 decode needs the field-type schema and the symbols.
  char *schema_blob = header->fmt_fieldtypes();
  char *symbols     = header->fmt_fieldlist();
  if (schema_blob == nullptr || symbols == nullptr) {
    return -1;
  }
  // Both must lie within the segment, including the fixed schema prefix.
  if (schema_blob < seg_start || schema_blob + sizeof(LogFieldTypeSchema) > seg_end || symbols < seg_start || symbols >= seg_end) {
    return -1;
  }

  // field_count is a uint16 that may sit at an unaligned offset (the writer
  // places the schema right after the NUL-terminated header strings), so read
  // it byte-wise rather than through the struct.
  uint16_t fc16 = 0;
  memcpy(&fc16, schema_blob, sizeof(fc16));
  const unsigned field_count = fc16;
  const uint8_t *codes       = reinterpret_cast<const uint8_t *>(schema_blob) + sizeof(LogFieldTypeSchema);
  if (reinterpret_cast<const char *>(codes) + field_count > seg_end) {
    return -1;
  }

  // field_count must match the symbol count; a mismatch is a corrupt segment.
  unsigned symbol_count = 0;
  bool     in_token     = false;
  for (const char *p = symbols; p < seg_end && *p != '\0'; ++p) {
    if (is_field_sep(*p)) {
      in_token = false;
    } else if (!in_token) {
      in_token = true;
      ++symbol_count;
    }
  }
  if (symbol_count != field_count) {
    return -1;
  }

  // Clamp value reads to this entry (or the segment, if entry_len is bogus).
  char *entry_start = reinterpret_cast<char *>(entry);
  if (entry_start < seg_start || entry_start + sizeof(LogEntryHeader) > seg_end) {
    return -1;
  }
  char *read_from = entry_start + sizeof(LogEntryHeader);
  char *read_end  = entry_start + entry->entry_len;
  if (read_end < read_from || read_end > seg_end) {
    read_end = seg_end;
  }

  int written = 0;

  // Append n bytes, keeping one byte in reserve for the trailing NUL.
  auto put = [&](const char *src, int n) -> bool {
    if (n < 0 || written + n >= buf_len) {
      return false;
    }
    memcpy(buf + written, src, n);
    written += n;
    return true;
  };
  auto put_ch = [&](char c) -> bool { return put(&c, 1); };

  // Emit the bytes in [start, end) as the body of a JSON string (the part
  // between the surrounding quotes). JSON requires escaping the quote,
  // backslash, and every control character (< 0x20); the latter use the short
  // forms where they exist and \u00XX otherwise. Bytes >= 0x20 pass through
  // verbatim, preserving the original (possibly non-UTF-8) bytes. Used for both
  // field values and the symbol keys, since a corrupt/untrusted segment may
  // carry arbitrary bytes in either.
  auto put_json_escaped = [&](const char *start, const char *end) -> bool {
    for (const char *p = start; p < end; ++p) {
      unsigned char c = static_cast<unsigned char>(*p);
      switch (c) {
      case '"':
        if (!put("\\\"", 2)) {
          return false;
        }
        break;
      case '\\':
        if (!put("\\\\", 2)) {
          return false;
        }
        break;
      case '\b':
        if (!put("\\b", 2)) {
          return false;
        }
        break;
      case '\f':
        if (!put("\\f", 2)) {
          return false;
        }
        break;
      case '\n':
        if (!put("\\n", 2)) {
          return false;
        }
        break;
      case '\r':
        if (!put("\\r", 2)) {
          return false;
        }
        break;
      case '\t':
        if (!put("\\t", 2)) {
          return false;
        }
        break;
      default:
        if (c < 0x20) {
          char esc[7];
          int  n = snprintf(esc, sizeof(esc), "\\u%04x", c);
          if (!put(esc, n)) {
            return false;
          }
        } else if (!put_ch(*p)) {
          return false;
        }
        break;
      }
    }
    return true;
  };

  if (!put_ch('{')) {
    return -1;
  }

  const char *sym = symbols;
  for (unsigned i = 0; i < field_count; ++i) {
    // Next symbol token (comma-separated in fmt_fieldlist), bounded by the segment.
    while (sym < seg_end && is_field_sep(*sym)) {
      ++sym;
    }
    const char *sym_start = sym;
    while (sym < seg_end && *sym != '\0' && !is_field_sep(*sym)) {
      ++sym;
    }

    if (i > 0 && !put_ch(',')) {
      return -1;
    }
    if (!put_ch('"') || !put_json_escaped(sym_start, sym) || !put_ch('"') || !put_ch(':')) {
      return -1;
    }

    // Dispatch on the framing type only, never the symbol; no field semantics
    // (see the contract in LogEntryJson.h).
    switch (static_cast<LogField::Type>(codes[i])) {
    case LogField::Type::sINT: {
      if (read_from + INK_MIN_ALIGN > read_end) {
        return -1;
      }
      int64_t v = LogAccess::unmarshal_int(&read_from);
      char    num[24]; // INT64_MIN is 20 digits + sign + NUL
      int     n = snprintf(num, sizeof(num), "%" PRId64, v);
      if (!put(num, n)) {
        return -1;
      }
      break;
    }
    case LogField::Type::STRING: {
      // NUL-terminated, 8-byte padded; require the terminator and padded field
      // within the entry.
      char *nul = static_cast<char *>(memchr(read_from, '\0', static_cast<size_t>(read_end - read_from)));
      if (nul == nullptr) {
        return -1;
      }
      const char *s          = read_from;
      size_t      padded_len = INK_ALIGN_DEFAULT(static_cast<size_t>(nul - read_from) + 1);
      if (read_from + padded_len > read_end) {
        return -1;
      }
      read_from += padded_len;
      if (!put_ch('"') || !put_json_escaped(s, nul) || !put_ch('"')) {
        return -1;
      }
      break;
    }
    case LogField::Type::IP: {
      // uint16 family + family-sized address, 8-byte padded.
      if (read_from + sizeof(LogFieldIp) > read_end) {
        return -1;
      }
      const LogFieldIp *ip  = reinterpret_cast<const LogFieldIp *>(read_from);
      size_t            len = sizeof(LogFieldIp);
      if (ip->_family == AF_INET) {
        len = sizeof(LogFieldIp4);
      } else if (ip->_family == AF_INET6) {
        len = sizeof(LogFieldIp6);
      } else if (ip->_family == AF_UNIX) {
        len = sizeof(LogFieldUn);
      }
      if (read_from + INK_ALIGN_DEFAULT(len) > read_end) {
        return -1;
      }
      char ip_str[INET6_ADDRSTRLEN + 1] = {0};
      int  n                            = LogAccess::unmarshal_ip_to_str(&read_from, ip_str, sizeof(ip_str) - 1);
      if (n < 0) {
        return -1;
      }
      if (!put_ch('"') || !put(ip_str, n) || !put_ch('"')) {
        return -1;
      }
      break;
    }
    case LogField::Type::dINT: {
      // Two int64s (16 bytes), rendered as a JSON array; meaning is the consumer's.
      if (read_from + 2 * INK_MIN_ALIGN > read_end) {
        return -1;
      }
      int64_t first  = LogAccess::unmarshal_int(&read_from);
      int64_t second = LogAccess::unmarshal_int(&read_from);
      char    num[48]; // two int64s, brackets, comma, NUL
      int     n = snprintf(num, sizeof(num), "[%" PRId64 ",%" PRId64 "]", first, second);
      if (!put(num, n)) {
        return -1;
      }
      break;
    }
    default:
      // INVALID or an unknown future type: length unknown, so we can't advance
      // safely. Bail rather than desync the entry.
      return -1;
    }
  }

  if (!put_ch('}')) {
    return -1;
  }
  buf[written] = '\0';
  return written;
}
