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