| /** @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; |
| } |