omni/structuredlog/JsonSerializer.h
File members: omni/structuredlog/JsonSerializer.h
// Copyright (c) 2020-2023, NVIDIA CORPORATION. All rights reserved.
//
// NVIDIA CORPORATION and its licensors retain all intellectual property
// and proprietary rights in and to this software, related documentation
// and any modifications thereto. Any use, reproduction, disclosure or
// distribution of this software and related documentation without an express
// license agreement from NVIDIA CORPORATION is strictly prohibited.
//
#pragma once
#include "../../carb/extras/StringSafe.h"
#include "../../carb/extras/Utf8Parser.h"
#include "../../carb/extras/Base64.h"
#include <stdint.h>
#include <float.h>
namespace omni
{
namespace structuredlog
{
class JsonConsumer
{
public:
virtual ~JsonConsumer()
{
}
virtual void consume(const char* json, size_t jsonLen) noexcept = 0;
virtual void terminate() noexcept = 0;
};
class JsonLengthCounter : public JsonConsumer
{
public:
void consume(const char* json, size_t jsonLen) noexcept override
{
CARB_UNUSED(json);
m_count += jsonLen;
}
void terminate() noexcept override
{
m_count++;
}
size_t getcount() noexcept
{
return m_count;
}
private:
size_t m_count = 0;
};
class JsonPrinter : public JsonConsumer
{
public:
JsonPrinter()
{
reset(nullptr, 0);
}
JsonPrinter(char* output, size_t outputLen) noexcept
{
reset(output, outputLen);
}
void reset(char* output, size_t outputLen) noexcept
{
m_output = (outputLen == 0) ? nullptr : output;
m_left = outputLen;
m_overflowed = false;
}
void consume(const char* json, size_t jsonLen) noexcept override
{
size_t w = CARB_MIN(m_left, jsonLen);
memcpy(m_output, json, w);
m_left -= w;
m_output += w;
m_overflowed = m_overflowed || w < jsonLen;
}
void terminate() noexcept override
{
if (m_output != nullptr)
{
if (m_left == 0)
{
m_output[-1] = '\0';
}
else
{
m_output[0] = '\0';
m_output++;
m_left--;
}
}
}
bool hasOverflowed() noexcept
{
return m_overflowed;
}
char* getNextChar() const noexcept
{
return m_output;
}
private:
char* m_output;
size_t m_left;
bool m_overflowed = false;
};
namespace
{
void ignoreJsonSerializerValidationError(const char* s) noexcept
{
CARB_UNUSED(s);
}
} // namespace
using OnValidationErrorFunc = void (*)(const char*);
template <bool validate = false, bool prettyPrint = false, OnValidationErrorFunc onValidationError = ignoreJsonSerializerValidationError>
class JsonSerializer
{
public:
OnValidationErrorFunc m_onValidationError = onValidationError;
JsonSerializer(JsonConsumer* consumer, size_t indentLen = 4) noexcept
{
CARB_ASSERT(consumer != nullptr);
m_consumer = consumer;
m_indentLen = indentLen;
}
~JsonSerializer()
{
// it starts to use heap memory at this point
if (m_scopes != m_scopesBuffer)
free(m_scopes);
}
void reset()
{
m_scopesTop = 0;
m_firstInScope = true;
m_hasKey = false;
m_firstPrint = true;
m_indentTotal = 0;
}
bool writeKey(const char* key, size_t keyLen) noexcept
{
if (validate)
{
if (CARB_UNLIKELY(getCurrentScope() != ScopeType::eObject))
{
char tmp[256];
carb::extras::formatString(tmp, sizeof(tmp),
"attempted to write a key outside an object"
" {key name = '%s', len = %zu}",
key, keyLen);
onValidationError(tmp);
return false;
}
if (CARB_UNLIKELY(m_hasKey))
{
char tmp[256];
carb::extras::formatString(tmp, sizeof(tmp),
"attempted to write out two key names in a row"
" {key name = '%s', len = %zu}",
key, keyLen);
onValidationError(tmp);
return false;
}
}
if (!m_firstInScope)
m_consumer->consume(",", 1);
prettyPrintHook();
m_consumer->consume("\"", 1);
if (key != nullptr)
m_consumer->consume(key, keyLen);
m_consumer->consume("\":", 2);
m_firstInScope = false;
if (validate)
m_hasKey = true;
return true;
}
bool writeKey(const char* key) noexcept
{
return writeKey(key, key == nullptr ? 0 : strlen(key));
}
bool writeValue() noexcept
{
if (CARB_UNLIKELY(!writeValuePrologue()))
return false;
prettyPrintValueHook();
m_consumer->consume("null", sizeof("null") - 1);
if (validate)
m_hasKey = false;
return true;
}
bool writeValue(bool value) noexcept
{
const char* val = value ? "true" : "false";
size_t len = (value ? sizeof("true") : sizeof("false")) - 1;
if (CARB_UNLIKELY(!writeValuePrologue()))
return false;
prettyPrintValueHook();
m_consumer->consume(val, len);
if (validate)
m_hasKey = false;
return true;
}
bool writeValue(int32_t value) noexcept
{
char buffer[32];
size_t i = 0;
if (CARB_UNLIKELY(!writeValuePrologue()))
return false;
i = carb::extras::formatString(buffer, CARB_COUNTOF(buffer), "%" PRId32, value);
prettyPrintValueHook();
m_consumer->consume(buffer, i);
if (validate)
m_hasKey = false;
return true;
}
bool writeValue(uint32_t value) noexcept
{
char buffer[32];
size_t i = 0;
if (CARB_UNLIKELY(!writeValuePrologue()))
return false;
i = carb::extras::formatString(buffer, CARB_COUNTOF(buffer), "%" PRIu32, value);
prettyPrintValueHook();
m_consumer->consume(buffer, i);
if (validate)
m_hasKey = false;
return true;
}
bool writeValue(int64_t value) noexcept
{
char buffer[32];
size_t i = 0;
if (CARB_UNLIKELY(!writeValuePrologue()))
return false;
i = carb::extras::formatString(buffer, CARB_COUNTOF(buffer), "%" PRId64, value);
prettyPrintValueHook();
m_consumer->consume(buffer, i);
if (validate)
m_hasKey = false;
return true;
}
bool writeValue(uint64_t value) noexcept
{
char buffer[32];
size_t i = 0;
if (CARB_UNLIKELY(!writeValuePrologue()))
return false;
i = carb::extras::formatString(buffer, CARB_COUNTOF(buffer), "%" PRIu64, value);
prettyPrintValueHook();
m_consumer->consume(buffer, i);
if (validate)
m_hasKey = false;
return true;
}
bool writeValue(double value) noexcept
{
char buffer[32];
size_t i = 0;
if (CARB_UNLIKELY(!writeValuePrologue()))
return false;
i = carb::extras::formatString(
buffer, CARB_COUNTOF(buffer), "%.*g", std::numeric_limits<double>::max_digits10, value);
prettyPrintValueHook();
m_consumer->consume(buffer, i);
if (validate)
m_hasKey = false;
return true;
}
bool writeValue(float value) noexcept
{
return writeValue(double(value));
}
bool writeValue(const char* value, size_t len) noexcept
{
size_t last = 0;
if (CARB_UNLIKELY(!writeValuePrologue()))
return false;
prettyPrintValueHook();
m_consumer->consume("\"", 1);
for (size_t i = 0; i < len;)
{
const carb::extras::Utf8Parser::CodeByte* next = carb::extras::Utf8Parser::nextCodePoint(value + i, len - i);
if (next == nullptr)
{
m_consumer->consume(value + last, i - last);
m_consumer->consume("\\u0000", 6);
i++;
last = i;
continue;
}
// early out for non-escape characters
// multi-byte characters never need to be escaped
if (size_t(next - value) > i + 1 || (value[i] > 0x1F && value[i] != '"' && value[i] != '\\'))
{
i = next - value;
continue;
}
m_consumer->consume(value + last, i - last);
switch (value[i])
{
case '"':
m_consumer->consume("\\\"", 2);
break;
case '\\':
m_consumer->consume("\\\\", 2);
break;
case '\b':
m_consumer->consume("\\b", 2);
break;
case '\f':
m_consumer->consume("\\f", 2);
break;
case '\n':
m_consumer->consume("\\n", 2);
break;
case '\r':
m_consumer->consume("\\r", 2);
break;
case '\t':
m_consumer->consume("\\t", 2);
break;
default:
{
char tmp[] = "\\u0000";
tmp[4] = getHexChar(uint8_t(value[i]) >> 4);
tmp[5] = getHexChar(value[i] & 0x0F);
m_consumer->consume(tmp, CARB_COUNTOF(tmp) - 1);
}
break;
}
i++;
last = i;
}
if (len > last)
m_consumer->consume(value + last, len - last);
m_consumer->consume("\"", 1);
if (validate)
m_hasKey = false;
return true;
}
bool writeValue(const char* value) noexcept
{
return writeValue(value, value == nullptr ? 0 : strlen(value));
}
bool writeValueWithBase64Encoding(const void* value_, size_t size)
{
char buffer[4096];
const size_t readSize = carb::extras::Base64::getEncodeInputSize(sizeof(buffer));
const uint8_t* value = static_cast<const uint8_t*>(value_);
if (CARB_UNLIKELY(!writeValuePrologue()))
return false;
m_consumer->consume("\"", 1);
for (size_t i = 0; i < size; i += readSize)
{
size_t written = m_base64Encoder.encode(value + i, CARB_MIN(readSize, size - i), buffer, sizeof(buffer));
m_consumer->consume(buffer, written);
}
m_consumer->consume("\"", 1);
if (validate)
m_hasKey = false;
return true;
}
bool openArray() noexcept
{
if (CARB_UNLIKELY(!writeValuePrologue()))
return false;
prettyPrintValueHook();
m_consumer->consume("[", 1);
m_firstInScope = true;
if (!pushScope(ScopeType::eArray))
return false;
prettyPrintOpenScope();
if (validate)
m_hasKey = false;
return true;
}
bool closeArray() noexcept
{
if (validate && CARB_UNLIKELY(getCurrentScope() != ScopeType::eArray))
{
onValidationError("attempted to close an array that was never opened");
return false;
}
popScope();
prettyPrintCloseScope();
prettyPrintHook();
m_consumer->consume("]", 1);
m_firstInScope = false;
if (validate)
m_hasKey = false;
return true;
}
bool openObject() noexcept
{
if (CARB_UNLIKELY(!writeValuePrologue()))
return false;
prettyPrintValueHook();
m_consumer->consume("{", 1);
m_firstInScope = true;
pushScope(ScopeType::eObject);
prettyPrintOpenScope();
if (validate)
m_hasKey = false;
return true;
}
bool closeObject() noexcept
{
if (validate && CARB_UNLIKELY(getCurrentScope() != ScopeType::eObject))
{
onValidationError("attempted to close an object that was never opened");
return false;
}
popScope();
prettyPrintCloseScope();
prettyPrintHook();
m_consumer->consume("}", 1);
m_firstInScope = false;
if (validate)
m_hasKey = false;
return true;
}
bool finish() noexcept
{
bool result = true;
// check whether we are ending in the middle of an object/array
if (validate && getCurrentScope() != ScopeType::eGlobal)
{
char tmp[256];
carb::extras::formatString(tmp, sizeof(tmp), "finished writing in the middle of an %s",
getCurrentScope() == ScopeType::eArray ? "array" : "object");
onValidationError(tmp);
result = false;
}
if (prettyPrint)
m_consumer->consume("\n", 1);
m_consumer->terminate();
return result;
}
private:
enum class ScopeType : uint8_t
{
eGlobal,
eArray,
eObject,
};
void prettyPrintHook() noexcept
{
if (prettyPrint)
{
const size_t s = CARB_COUNTOF(m_indent) - 1;
if (!m_firstPrint)
m_consumer->consume("\n", 1);
m_firstPrint = false;
for (size_t i = s; i <= m_indentTotal; i += s)
m_consumer->consume(m_indent, s);
m_consumer->consume(m_indent, m_indentTotal % s);
}
}
void prettyPrintValueHook() noexcept
{
if (prettyPrint)
{
if (getCurrentScope() != ScopeType::eObject)
return prettyPrintHook();
else
/* if it's in an object, a key preceded this so this should be on the same line */
m_consumer->consume(" ", 1);
}
}
void prettyPrintOpenScope() noexcept
{
if (prettyPrint)
m_indentTotal += m_indentLen;
}
void prettyPrintCloseScope() noexcept
{
if (prettyPrint)
m_indentTotal -= m_indentLen;
}
inline bool writeValuePrologue() noexcept
{
if (validate)
{
// the global scope is only allowed to have one item in it
if (CARB_UNLIKELY(getCurrentScope() == ScopeType::eGlobal && !m_firstInScope))
{
onValidationError("attempted to put multiple values into the global scope");
return false;
}
// if we're in an object, a key needs to have been written before each value
if (CARB_UNLIKELY(getCurrentScope() == ScopeType::eObject && !m_hasKey))
{
onValidationError("attempted to write a value without a key inside an object");
return false;
}
}
if (getCurrentScope() == ScopeType::eArray && !m_firstInScope)
m_consumer->consume(",", 1);
m_firstInScope = false;
return true;
}
bool pushScope(ScopeType s) noexcept
{
if (m_scopesTop == m_scopesLen)
{
size_t newLen = m_scopesTop + 64;
size_t size = sizeof(*m_scopes) * newLen;
void* tmp;
if (m_scopes == m_scopesBuffer)
tmp = malloc(size);
else
tmp = realloc(m_scopes, size);
if (tmp == nullptr)
{
char log[256];
carb::extras::formatString(log, sizeof(log), "failed to allocate %zu bytes", size);
onValidationError(log);
return false;
}
if (m_scopes == m_scopesBuffer)
memcpy(tmp, m_scopes, sizeof(*m_scopes) * m_scopesLen);
m_scopes = static_cast<ScopeType*>(tmp);
m_scopesLen = newLen;
}
m_scopes[m_scopesTop++] = s;
return true;
}
void popScope() noexcept
{
m_scopesTop--;
}
ScopeType getCurrentScope() noexcept
{
if (m_scopesTop == 0)
return ScopeType::eGlobal;
return m_scopes[m_scopesTop - 1];
}
char getHexChar(uint8_t c) noexcept
{
char lookup[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
CARB_ASSERT(c < CARB_COUNTOF(lookup));
return lookup[c];
}
ScopeType m_scopesBuffer[8];
ScopeType* m_scopes = m_scopesBuffer;
size_t m_scopesLen = CARB_COUNTOF(m_scopesBuffer);
size_t m_scopesTop = 0;
JsonConsumer* m_consumer;
bool m_firstInScope = true;
bool m_hasKey = false;
bool m_firstPrint = true;
char m_indent[33] = " ";
size_t m_indentTotal = 0;
size_t m_indentLen = 4;
carb::extras::Base64 m_base64Encoder;
};
} // namespace structuredlog
} // namespace omni