omni/structuredlog/JsonTree.h

File members: omni/structuredlog/JsonTree.h

// Copyright (c) 2020-2024, 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/logging/Log.h"
#include "../extras/ScratchBuffer.h"

#include <cstdint>
#include <cstddef>

namespace omni
{
namespace structuredlog
{

enum class NodeType : uint8_t
{
    eNull,
    eBool,
    eBoolArray,
    eInt32,
    eInt32Array,
    eUint32,
    eUint32Array,
    eInt64,

    eInt64Array,

    eUint64,
    eUint64Array,
    eFloat64,
    eFloat64Array,
    eFloat32,
    eFloat32Array,
    eBinary,
    eString,
    eStringArray,
    eObject,
    eObjectArray,
};

struct JsonNode
{
    static constexpr uint32_t kVersion = 0;

    using EnumBase = uint16_t;

    using Flag = uint8_t;

    static constexpr Flag fFlagConst = 0x01;

    static constexpr Flag fFlagFixedLength = 0x02;

    static constexpr Flag fFlagEnum = 0x04;

    NodeType type = NodeType::eNull;

    Flag flags = 0;

    uint16_t len = 0;

    uint16_t nameLen = 0;

    /* 2 bytes of padding exists here (on x86_64 at least), which may be used
     * for something in the future without breaking the ABI. */

    char* name = nullptr;

    union
    {
        bool boolVal;

        int64_t intVal;

        uint64_t uintVal;

        double floatVal;

        uint8_t* binaryVal;

        char* strVal;

        bool* boolArrayVal;

        int32_t* int32ArrayVal;

        int64_t* int64ArrayVal;

        uint32_t* uint32ArrayVal;

        uint64_t* uint64ArrayVal;

        float* float32ArrayVal;

        double* float64ArrayVal;

        char** strArrayVal;

        JsonNode* objVal;
    } data;
};

CARB_ASSERT_INTEROP_SAFE(JsonNode);
static_assert(sizeof(JsonNode) == 24, "unexpected size");
static_assert(offsetof(JsonNode, type) == 0, "struct layout changed");
static_assert(offsetof(JsonNode, flags) == 1, "struct layout changed");
static_assert(offsetof(JsonNode, len) == 2, "struct layout changed");
static_assert(offsetof(JsonNode, nameLen) == 4, "struct layout changed");
static_assert(offsetof(JsonNode, name) == 8, "struct layout changed");
static_assert(offsetof(JsonNode, data) == 16, "struct layout changed");

enum class JsonTreeCompareFuzz
{
    eStrict,

    eNoConstOrder,

    eNoOrder,
};

inline bool compareJsonTrees(const JsonNode* a, const JsonNode* b, JsonTreeCompareFuzz fuzz = JsonTreeCompareFuzz::eStrict)
{
    if (a->flags != b->flags || a->type != b->type || a->len != b->len)
        return false;

    if ((a->name == nullptr && b->name != nullptr) || (a->name != nullptr && b->name == nullptr))
        return false;

    if (a->name != nullptr && strcmp(a->name, b->name) != 0)
        return false;

    switch (a->type)
    {
        case NodeType::eNull:
            return true;

        case NodeType::eBool:
            return a->data.boolVal == b->data.boolVal;

        case NodeType::eInt32:
        case NodeType::eInt64:
            return a->data.intVal == b->data.intVal;

        case NodeType::eUint32:
        case NodeType::eUint64:
            return a->data.uintVal == b->data.uintVal;

        case NodeType::eFloat32:
        case NodeType::eFloat64:
            return a->data.floatVal == b->data.floatVal;

        case NodeType::eBoolArray:
            return a->len == 0 || memcmp(b->data.int32ArrayVal, a->data.int32ArrayVal, a->len * sizeof(bool)) == 0;

        case NodeType::eUint32Array:
            return a->len == 0 || memcmp(b->data.uint32ArrayVal, a->data.uint32ArrayVal, a->len * sizeof(uint32_t)) == 0;

        case NodeType::eInt32Array:
            return a->len == 0 || memcmp(b->data.int32ArrayVal, a->data.int32ArrayVal, a->len * sizeof(int32_t)) == 0;

        case NodeType::eFloat32Array:
            return a->len == 0 || memcmp(b->data.float32ArrayVal, a->data.float32ArrayVal, a->len * sizeof(float)) == 0;

        case NodeType::eInt64Array:
            return a->len == 0 || memcmp(b->data.int64ArrayVal, a->data.int64ArrayVal, a->len * sizeof(int64_t)) == 0;

        case NodeType::eUint64Array:
            return a->len == 0 || memcmp(b->data.uint64ArrayVal, a->data.uint64ArrayVal, a->len * sizeof(uint64_t)) == 0;

        case NodeType::eFloat64Array:
            return a->len == 0 || memcmp(b->data.float64ArrayVal, a->data.float64ArrayVal, a->len * sizeof(double)) == 0;

        case NodeType::eBinary:
        case NodeType::eString:
            return a->len == 0 || memcmp(b->data.binaryVal, a->data.binaryVal, a->len) == 0;

        case NodeType::eStringArray:
            for (uint16_t i = 0; i < a->len; i++)
            {
                if ((a->data.strArrayVal[i] == nullptr && b->data.strArrayVal[i] != nullptr) ||
                    (a->data.strArrayVal[i] != nullptr && b->data.strArrayVal[i] == nullptr))
                    return false;

                if (a->data.strArrayVal[i] != nullptr && strcmp(a->data.strArrayVal[i], b->data.strArrayVal[i]) != 0)
                    return false;
            }
            return true;

        case NodeType::eObject:
        case NodeType::eObjectArray:
            switch (fuzz)
            {
                case JsonTreeCompareFuzz::eStrict:
                    for (uint16_t i = 0; i < a->len; i++)
                    {
                        if (!compareJsonTrees(&a->data.objVal[i], &b->data.objVal[i]))
                            return false;
                    }
                    break;

                case JsonTreeCompareFuzz::eNoConstOrder:
                {
                    extras::ScratchBuffer<bool, 256> hits;
                    if (!hits.resize(a->len))
                    {
                        // shouldn't ever happen
                        fprintf(stderr, "failed to allocate %u bytes\n", a->len);
                        return false;
                    }

                    for (size_t i = 0; i < a->len; i++)
                    {
                        hits[i] = false;
                    }

                    // first compare the variable fields in order
                    for (uint16_t i = 0, j = 0; i < a->len; i++, j++)
                    {
                        // skip to the next non-constant member
                        while ((a->data.objVal[i].flags & JsonNode::fFlagConst) != 0)
                        {
                            i++;
                        }
                        if (i >= a->len)
                            break;

                        // skip to the next non-constant member
                        while ((b->data.objVal[j].flags & JsonNode::fFlagConst) != 0)
                        {
                            j++;
                        }
                        if (j >= b->len)
                            return false;

                        if (!compareJsonTrees(&a->data.objVal[i], &b->data.objVal[j]))
                            return false;
                    }

                    // compare the constants
                    for (uint16_t i = 0; i < a->len; i++)
                    {
                        if ((a->data.objVal[i].flags & JsonNode::fFlagConst) == 0)
                            continue;

                        for (uint16_t j = 0; j < a->len; j++)
                        {
                            if (!hits[j] && (b->data.objVal[j].flags & JsonNode::fFlagConst) != 0 &&
                                compareJsonTrees(&a->data.objVal[i], &b->data.objVal[j]))
                            {
                                hits[j] = true;
                                break;
                            }
                        }
                    }

                    for (uint16_t i = 0; i < a->len; i++)
                    {
                        if ((b->data.objVal[i].flags & JsonNode::fFlagConst) != 0 && !hits[i])
                            return false;
                    }
                }
                break;

                case JsonTreeCompareFuzz::eNoOrder:
                {
                    extras::ScratchBuffer<bool, 256> hits;
                    if (!hits.resize(a->len))
                    {
                        // shouldn't ever happen
                        fprintf(stderr, "failed to allocate %u bytes\n", a->len);
                        return false;
                    }

                    for (size_t i = 0; i < a->len; i++)
                    {
                        hits[i] = false;
                    }

                    for (uint16_t i = 0; i < a->len; i++)
                    {
                        for (uint16_t j = 0; j < a->len; j++)
                        {
                            if (!hits[j] && compareJsonTrees(&a->data.objVal[i], &b->data.objVal[j]))
                            {
                                hits[j] = true;
                                break;
                            }
                        }
                    }

                    for (uint16_t i = 0; i < a->len; i++)
                    {
                        if (!hits[i])
                            return false;
                    }
                }
                break;
            }
            return true;
    }
    return false;
}

class Allocator
{
public:
    static constexpr size_t kAlignment = alignof(void*);

    virtual ~Allocator()
    {
    }

    virtual void* alloc(size_t size)
    {
        return malloc(size);
    }

    virtual void dealloc(void* mem)
    {
        free(mem);
    }

    static size_t fixupAlignment(size_t size)
    {
        if (size % Allocator::kAlignment != 0)
            return size + (kAlignment - (size % Allocator::kAlignment));
        else
            return size;
    }
};

class BlockAllocator : public Allocator
{
public:
    BlockAllocator(void* block, size_t len)
    {
        m_block = static_cast<uint8_t*>(block);
        m_left = len;
    }

    void* alloc(size_t size) override
    {
        void* m = m_block;

        size = fixupAlignment(size);
        if (size > m_left)
            return nullptr;

        m_block += size;
        m_left -= size;
        return m;
    }

    void dealloc(void* mem) override
    {
        CARB_UNUSED(mem);
        // leak it - m_block will be freed when we're done with this allocator
    }

private:
    uint8_t* m_block = nullptr;

    size_t m_left = 0;
};

static inline void clearJsonTree(JsonNode* node, Allocator* alloc)
{
    switch (node->type)
    {
        case NodeType::eNull:
        case NodeType::eBool:
        case NodeType::eInt32:
        case NodeType::eUint32:
        case NodeType::eInt64:
        case NodeType::eUint64:
        case NodeType::eFloat32:
        case NodeType::eFloat64:
            break;

        case NodeType::eString:
        case NodeType::eBinary:
        case NodeType::eBoolArray:
        case NodeType::eInt32Array:
        case NodeType::eUint32Array:
        case NodeType::eInt64Array:
        case NodeType::eUint64Array:
        case NodeType::eFloat32Array:
        case NodeType::eFloat64Array:
            alloc->dealloc(node->data.strVal);
            node->data.strVal = nullptr;
            break;

        case NodeType::eStringArray:
            for (uint16_t i = 0; i < node->len; i++)
            {
                alloc->dealloc(node->data.strArrayVal[i]);
            }
            alloc->dealloc(node->data.strArrayVal);
            node->data.strArrayVal = nullptr;
            break;

        case NodeType::eObjectArray:
            // object arrays allocate their elements and their elements'
            // properties in the same allocation
            for (uint16_t i = 0; i < node->len; i++)
            {
                for (uint16_t j = 0; j < node->data.objVal[i].len; j++)
                    clearJsonTree(&node->data.objVal[i].data.objVal[j], alloc);
            }

            alloc->dealloc(node->data.objVal);
            node->data.objVal = nullptr;
            break;

        case NodeType::eObject:
            for (uint16_t i = 0; i < node->len; i++)
                clearJsonTree(&node->data.objVal[i], alloc);

            alloc->dealloc(node->data.objVal);
            node->data.objVal = nullptr;
            break;
    }
    alloc->dealloc(node->name);

    node = {};
}

class TempJsonNode : public JsonNode
{
public:
    TempJsonNode(Allocator* alloc)
    {
        m_alloc = alloc;
    }

    ~TempJsonNode()
    {
        clearJsonTree(this, m_alloc);
    }

private:
    Allocator* m_alloc;
};

class JsonTreeSizeCalculator
{
public:
    size_t getSize()
    {
        return m_count;
    }

    void trackRoot()
    {
        m_count += sizeof(JsonNode);
    }

    void trackObject(size_t propertyCount)
    {
        m_count += Allocator::fixupAlignment(sizeof(JsonNode) * propertyCount);
    }

    void trackObjectArray(size_t propertyCount, size_t len)
    {
        m_count += Allocator::fixupAlignment(sizeof(JsonNode) * (propertyCount + 1) * len);
    }

    void trackName(const char* name, uint16_t nameLen)
    {
        track(name, nameLen);
    }

    void trackName(const char* name)
    {
        track(name);
    }

    void track()
    {
    }

    template <typename T>
    void track(T value)
    {
        CARB_UNUSED(value);
        static_assert(std::is_arithmetic<T>::value, "this is only valid for primitive types");
    }

    void track(const char* const* str, uint16_t len)
    {
        size_t size = 0;
        if (len == 0)
            return;

        for (uint16_t i = 0; i < len; i++)
        {
            if (str[i] != nullptr)
                size += Allocator::fixupAlignment(strlen(str[i]) + 1);
        }

        m_count += size + Allocator::fixupAlignment(sizeof(const char*) * len);
    }

    template <typename T>
    void track(const T* value, uint16_t len)
    {
        CARB_UNUSED(value);
        static_assert(std::is_arithmetic<T>::value, "this is only valid for primitive types");
        m_count += Allocator::fixupAlignment(len * sizeof(T));
    }

    void track(const void* value, uint16_t len)
    {
        track(static_cast<const uint8_t*>(value), len);
    }

    void track(void* value, uint16_t len)
    {
        track(static_cast<const uint8_t*>(value), len);
    }

    void track(const char* str)
    {
        if (str != nullptr)
            track(str, uint16_t(strlen(str) + 1));
    }

    void track(char* str)
    {
        track(static_cast<const char*>(str));
    }

    void track(const JsonNode* node)
    {
        trackName(node->name);
        switch (node->type)
        {
            case NodeType::eNull:
                track();
                break;

            case NodeType::eBool:
                track(node->data.boolVal);
                break;

            case NodeType::eInt32:
            case NodeType::eInt64:
                track(node->data.intVal);
                break;

            case NodeType::eUint32:
            case NodeType::eUint64:
                track(node->data.uintVal);
                break;

            case NodeType::eFloat32:
            case NodeType::eFloat64:
                track(node->data.floatVal);
                break;

            case NodeType::eBinary:
                track(node->data.binaryVal, node->len);
                break;

            case NodeType::eBoolArray:
                track(node->data.boolArrayVal, node->len);
                break;

            case NodeType::eInt32Array:
                track(node->data.uint32ArrayVal, node->len);
                break;

            case NodeType::eUint32Array:
                track(node->data.int32ArrayVal, node->len);
                break;

            case NodeType::eInt64Array:
                track(node->data.uint64ArrayVal, node->len);
                break;

            case NodeType::eUint64Array:
                track(node->data.int64ArrayVal, node->len);
                break;

            case NodeType::eFloat32Array:
                track(node->data.float32ArrayVal, node->len);
                break;

            case NodeType::eFloat64Array:
                track(node->data.float64ArrayVal, node->len);
                break;

            case NodeType::eString:
                track(node->data.strVal, node->len);
                break;

            case NodeType::eStringArray:
                // if you don't cast this, it doesn't infer the template correctly
                track(static_cast<const char* const*>(node->data.strArrayVal), node->len);
                break;

            case NodeType::eObjectArray:
                if (node->len > 0)
                    trackObjectArray(node->data.objVal[0].len, node->len);

                for (size_t i = 0; i < node->len; i++)
                {
                    for (size_t j = 0; j < node->data.objVal[i].len; j++)
                        // if you don't cast this, it doesn't infer the template correctly
                        track(static_cast<const JsonNode*>(&node->data.objVal[i].data.objVal[j]));
                }
                break;

            case NodeType::eObject:
                trackObject(node->len);
                for (size_t i = 0; i < node->len; i++)
                    // if you don't cast this, it doesn't infer the template correctly
                    track(static_cast<const JsonNode*>(&node->data.objVal[i]));
                break;
        }
    }

    void track(JsonNode* node)
    {
        return track(static_cast<const JsonNode*>(node));
    }

private:
    size_t m_count = 0;
};

class JsonBuilder
{
public:
    JsonBuilder(Allocator* alloc)
    {
        m_alloc = alloc;
    }

    bool createObject(JsonNode* node, uint16_t propertyCount)
    {
        void* b;

        CARB_ASSERT(node->type == NodeType::eNull);
        CARB_ASSERT(node->len == 0);

        b = m_alloc->alloc(propertyCount * sizeof(*node->data.objVal));
        if (b == nullptr)
        {
            CARB_LOG_ERROR("allocator ran out of memory (node = '%s', requested %zu bytes)", node->name,
                           propertyCount * sizeof(*node->data.objVal));
            return false;
        }

        node->data.objVal = new (b) JsonNode[propertyCount];
        node->len = propertyCount;
        node->type = NodeType::eObject;

        CARB_ASSERT((uintptr_t(node->data.objVal) & (alignof(JsonNode) - 1)) == 0);

        return true;
    }

    bool createObjectArray(JsonNode* node, uint16_t propertyCount, uint16_t len)
    {
        void* b;

        CARB_ASSERT(node->type == NodeType::eNull);
        CARB_ASSERT(node->len == 0);

        // allocate it all as one array
        b = m_alloc->alloc((len * (1 + propertyCount)) * sizeof(*node->data.objVal));
        if (b == nullptr)
        {
            CARB_LOG_ERROR("allocator ran out of memory (node = '%s', requested %zu bytes)", node->name,
                           (len * (1 + propertyCount)) * sizeof(*node->data.objVal));
            return false;
        }

        node->data.objVal = new (b) JsonNode[len];
        CARB_ASSERT((uintptr_t(node->data.objVal) & (alignof(JsonNode) - 1)) == 0);
        b = static_cast<uint8_t*>(b) + sizeof(JsonNode) * len;

        for (size_t i = 0; i < len; i++)
        {
            node->data.objVal[i].data.objVal = new (b) JsonNode[propertyCount];
            CARB_ASSERT((uintptr_t(node->data.objVal[i].data.objVal) & (alignof(JsonNode) - 1)) == 0);
            b = static_cast<uint8_t*>(b) + sizeof(JsonNode) * propertyCount;

            node->data.objVal[i].len = propertyCount;
            node->data.objVal[i].type = NodeType::eObject;
        }

        node->len = len;
        node->type = NodeType::eObjectArray;

        return true;
    }

    bool setNode(JsonNode* node, bool b)
    {
        return setNode(node, b, &node->data.boolVal, NodeType::eBool);
    }

    bool setNode(JsonNode* node, const bool* data, uint16_t len)
    {
        return setNode(node, data, len, &node->data.boolArrayVal, NodeType::eBoolArray);
    }

    bool setNode(JsonNode* node, int32_t i)
    {
        return setNode(node, i, &node->data.intVal, NodeType::eInt32);
    }

    bool setNode(JsonNode* node, const int32_t* data, uint16_t len)
    {
        return setNode(node, data, len, &node->data.int32ArrayVal, NodeType::eInt32Array);
    }

    bool setNode(JsonNode* node, uint32_t u)
    {
        return setNode(node, u, &node->data.uintVal, NodeType::eUint32);
    }

    bool setNode(JsonNode* node, const uint32_t* data, uint16_t len)
    {
        return setNode(node, data, len, &node->data.uint32ArrayVal, NodeType::eUint32Array);
    }

    bool setNode(JsonNode* node, int64_t i)
    {
        return setNode(node, i, &node->data.intVal, NodeType::eInt64);
    }

    bool setNode(JsonNode* node, const int64_t* data, uint16_t len)
    {
        return setNode(node, data, len, &node->data.int64ArrayVal, NodeType::eInt64Array);
    }

    bool setNode(JsonNode* node, uint64_t u)
    {
        return setNode(node, u, &node->data.uintVal, NodeType::eUint64);
    }

    bool setNode(JsonNode* node, const uint64_t* data, uint16_t len)
    {
        return setNode(node, data, len, &node->data.uint64ArrayVal, NodeType::eUint64Array);
    }

    bool setNode(JsonNode* node, float f)
    {
        return setNode(node, f, &node->data.floatVal, NodeType::eFloat32);
    }

    bool setNode(JsonNode* node, const float* data, uint16_t len)
    {
        return setNode(node, data, len, &node->data.float32ArrayVal, NodeType::eFloat32Array);
    }

    bool setNode(JsonNode* node, double f)
    {
        return setNode(node, f, &node->data.floatVal, NodeType::eFloat64);
    }

    bool setNode(JsonNode* node, const double* data, uint16_t len)
    {
        return setNode(node, data, len, &node->data.float64ArrayVal, NodeType::eFloat64Array);
    }

    bool setNode(JsonNode* node, const char* const* data, uint16_t len)
    {
        CARB_ASSERT(node->type == NodeType::eNull);
        CARB_ASSERT(node->len == 0);

        if (len == 0)
        {
            node->data.strArrayVal = nullptr;
            node->type = NodeType::eStringArray;
            node->len = len;
            return true;
        }

        // allocate each of the strings individually to avoid having to create
        // a temporary buffer of string lengths or call strlen() multiple times
        // on each string
        node->data.strArrayVal = static_cast<char**>(m_alloc->alloc(len * sizeof(*node->data.strArrayVal)));
        CARB_ASSERT((uintptr_t(node->data.strArrayVal) & (alignof(char*) - 1)) == 0);
        if (node->data.strArrayVal == nullptr)
        {
            CARB_LOG_ERROR("allocator ran out of memory (node = '%s', requested %zu bytes)", node->name,
                           len * sizeof(*node->data.strArrayVal));
            return false;
        }

        for (uint16_t i = 0; i < len; i++)
        {
            if (data[i] == nullptr)
            {
                node->data.strArrayVal[i] = nullptr;
            }
            else
            {
                size_t s = strlen(data[i]) + 1;
                node->data.strArrayVal[i] = static_cast<char*>(m_alloc->alloc(s));
                if (node->data.strArrayVal[i] == nullptr)
                {
                    CARB_LOG_ERROR("allocator ran out of memory (requested %zu bytes)", s);
                    for (uint16_t j = 0; j < i; j++)
                        m_alloc->dealloc(node->data.strArrayVal[j]);
                    m_alloc->dealloc(node->data.strArrayVal);
                    node->data.strArrayVal = nullptr;
                    return false;
                }

                memcpy(node->data.strArrayVal[i], data[i], s);
            }
        }

        node->type = NodeType::eStringArray;
        node->len = len;

        return true;
    }

    bool setNode(JsonNode* node, const char* str, uint16_t len)
    {
        return setNode(node, str, len, &node->data.strVal, NodeType::eString);
    }

    bool setNode(JsonNode* node, const char* str)
    {
        size_t len;

        CARB_ASSERT(node->type == NodeType::eNull);
        CARB_ASSERT(node->len == 0);

        if (str == nullptr)
            return setNode(node, str, 0);

        len = strlen(str) + 1;
        if (len > UINT16_MAX)
        {
            CARB_LOG_ERROR("string length exceeds 64KiB maximum (node = '%s', %zu characters, str = '%64s...')",
                           node->name, len, str);
            return false;
        }

        return setNode(node, str, uint16_t(len));
    }

    bool setNode(JsonNode* node, const void* blob, uint16_t len)
    {
        return setNode<uint8_t>(node, static_cast<const uint8_t*>(blob), len, &node->data.binaryVal, NodeType::eBinary);
    }

    bool setNode(JsonNode* node, const uint8_t* blob, uint16_t len)
    {
        return setNode<uint8_t>(node, blob, len, &node->data.binaryVal, NodeType::eBinary);
    }

    bool setName(JsonNode* node, const char* name, uint16_t nameLen)
    {
        m_alloc->dealloc(node->name);
        node->name = nullptr;

        if (nameLen == 0)
            return true;

        node->name = static_cast<char*>(m_alloc->alloc(nameLen));
        if (node->name == nullptr)
        {
            CARB_LOG_ERROR("allocator ran out of memory (name = '%s', requested %" PRIu16 " bytes)", name, nameLen);
            return false;
        }

        node->nameLen = nameLen;
        memcpy(node->name, name, nameLen);
        return true;
    }

    bool setName(JsonNode* node, const char* name)
    {
        if (name == nullptr)
            return setName(node, nullptr, 0);

        return setName(node, name, uint16_t(strlen(name) + 1));
    }

    bool deepCopy(const JsonNode* node, JsonNode* out)
    {
        bool result = true;

        CARB_ASSERT(out->type == NodeType::eNull);
        CARB_ASSERT(out->len == 0);

        if (!setName(out, node->name))
            return false;

        out->flags = node->flags;
        switch (node->type)
        {
            case NodeType::eNull:
                break;

            case NodeType::eBool:
                result = setNode(out, node->data.boolVal);
                break;

            case NodeType::eBoolArray:
                result = setNode(out, node->data.boolArrayVal, node->len);
                break;

            case NodeType::eInt32:
                result = setNode(out, int32_t(node->data.intVal));
                break;

            case NodeType::eInt64:
                result = setNode(out, node->data.intVal);
                break;

            case NodeType::eInt32Array:
                result = setNode(out, node->data.int32ArrayVal, node->len);
                break;

            case NodeType::eUint32:
                result = setNode(out, uint32_t(node->data.uintVal));
                break;

            case NodeType::eUint64:
                result = setNode(out, node->data.uintVal);
                break;

            case NodeType::eUint32Array:
                result = setNode(out, node->data.uint32ArrayVal, node->len);
                break;

            case NodeType::eInt64Array:
                result = setNode(out, node->data.int64ArrayVal, node->len);
                break;

            case NodeType::eUint64Array:
                result = setNode(out, node->data.uint64ArrayVal, node->len);
                break;

            case NodeType::eFloat32:
                result = setNode(out, float(node->data.floatVal));
                break;

            case NodeType::eFloat64:
                result = setNode(out, node->data.floatVal);
                break;

            case NodeType::eFloat32Array:
                result = setNode(out, node->data.float32ArrayVal, node->len);
                break;

            case NodeType::eFloat64Array:
                result = setNode(out, node->data.float64ArrayVal, node->len);
                break;

            case NodeType::eString:
                result = setNode(out, node->data.strVal, node->len);
                break;

            case NodeType::eStringArray:
                result = setNode(out, node->data.strArrayVal, node->len);
                break;

            case NodeType::eBinary:
                result = setNode(out, node->data.binaryVal, node->len);
                break;

            case NodeType::eObjectArray:
                if (node->len == 0)
                    break;

                result = createObjectArray(out, node->data.objVal[0].len, node->len);
                if (!result)
                    break;

                for (uint16_t i = 0; i < node->len; i++)
                {
                    out->data.objVal[i].flags = node->data.objVal[i].flags;
                    for (uint16_t j = 0; result && j < node->data.objVal[i].len; j++)
                    {
                        result = deepCopy(&node->data.objVal[i].data.objVal[j], &out->data.objVal[i].data.objVal[j]);
                    }
                }
                break;

            case NodeType::eObject:
                result = createObject(out, node->len);
                if (!result)
                    break;

                for (size_t i = 0; i < node->len; i++)
                {
                    deepCopy(&node->data.objVal[i], &out->data.objVal[i]);
                }
                break;
        }

        if (!result)
            clearJsonTree(out, m_alloc);

        return result;
    }

    template <bool failOnNonFatal = false>
    static bool setFlags(JsonNode* node, JsonNode::Flag flags)
    {
        constexpr JsonNode::Flag fixedLengthConst = JsonNode::fFlagConst | JsonNode::fFlagFixedLength;
        constexpr JsonNode::Flag constEnum = JsonNode::fFlagEnum | JsonNode::fFlagConst;
        constexpr JsonNode::Flag validFlags = JsonNode::fFlagConst | JsonNode::fFlagFixedLength | JsonNode::fFlagEnum;
        bool result = true;

        CARB_ASSERT(node != nullptr);

        // filter out unknown flags because they pose an ABI risk
        if ((flags & ~validFlags) != 0)
        {
            CARB_LOG_ERROR("unknown flags were used %02x (node = '%s')", flags & ~validFlags, node->name);
            return false;
        }

        // a node cannot be fixed length and constant
        if ((flags & ~fixedLengthConst) == fixedLengthConst)
        {
            CARB_LOG_ERROR("attempted to set node to be both const and fixed length (node = '%s')", node->name);
            result = !failOnNonFatal;
        }

        if ((flags & constEnum) == constEnum)
        {
            CARB_LOG_ERROR("a node cannot be both constant and an enum (node = '%s')", node->name);
            flags &= JsonNode::Flag(~JsonNode::fFlagConst);
            result = !failOnNonFatal;
        }

        if ((flags & JsonNode::fFlagEnum) != 0 && node->len == 0)
        {
            CARB_LOG_ERROR("an empty array can not be made into an enum (node = '%s')", node->name);
            return false;
        }

        // check for invalid enum type usage
        switch (node->type)
        {
            case NodeType::eNull:
            case NodeType::eBool:
            case NodeType::eInt32:
            case NodeType::eUint32:
            case NodeType::eInt64:
            case NodeType::eUint64:
            case NodeType::eFloat64:
            case NodeType::eFloat32:
            case NodeType::eObject:
            case NodeType::eObjectArray:
            case NodeType::eBinary:
            case NodeType::eString:
                if ((flags & JsonNode::fFlagEnum) != 0)
                {
                    CARB_LOG_ERROR("an enum type must be on a non-object array type (node = '%s')", node->name);
                    return false;
                }
                break;

            case NodeType::eBoolArray:
            case NodeType::eInt32Array:
            case NodeType::eUint32Array:
            case NodeType::eInt64Array:
            case NodeType::eUint64Array:
            case NodeType::eFloat64Array:
            case NodeType::eFloat32Array:
            case NodeType::eStringArray:
                // arrays can be const or fixed length
                break;
        }

        // check for invalid const or fixed length usage
        switch (node->type)
        {
            case NodeType::eNull:
            case NodeType::eBool:
            case NodeType::eInt32:
            case NodeType::eUint32:
            case NodeType::eInt64:
            case NodeType::eUint64:
            case NodeType::eFloat64:
            case NodeType::eFloat32:
                // fixed length is only valid on arrays
                if ((flags & JsonNode::fFlagFixedLength) != 0)
                {
                    CARB_LOG_ERROR("fixed length cannot be set on a scalar node (node = '%s')", node->name);
                    result = !failOnNonFatal;
                }
                break;

            case NodeType::eObject:
                if ((flags & JsonNode::fFlagConst) != 0)
                {
                    CARB_LOG_ERROR("const is meaningless on an object node (node = '%s')", node->name);
                    result = !failOnNonFatal;
                }

                if ((flags & JsonNode::fFlagFixedLength) != 0)
                {
                    CARB_LOG_ERROR("const is meaningless on an object node (node = '%s')", node->name);
                    result = !failOnNonFatal;
                }
                break;

            case NodeType::eObjectArray:
                if ((flags & JsonNode::fFlagConst) != 0)
                {
                    CARB_LOG_ERROR("const is meaningless on an object array (node = '%s')", node->name);
                    result = !failOnNonFatal;
                }
                break;

            case NodeType::eBinary:
            case NodeType::eString:

            case NodeType::eBoolArray:
            case NodeType::eInt32Array:
            case NodeType::eUint32Array:
            case NodeType::eInt64Array:
            case NodeType::eUint64Array:
            case NodeType::eFloat64Array:
            case NodeType::eFloat32Array:
            case NodeType::eStringArray:
                // arrays can be const or fixed length
                break;
        }

        node->flags = flags;
        return result;
    }

private:
    template <typename T0, typename T1>
    bool setNode(JsonNode* node, T0 value, T1* dest, NodeType type)
    {
        CARB_ASSERT(node->type == NodeType::eNull);
        CARB_ASSERT(node->len == 0);

        node->len = 1;
        node->type = type;
        *dest = value;

        return true;
    }

    template <typename T>
    bool setNode(JsonNode* node, const T* data, uint16_t len, T** dest, NodeType type)
    {
        CARB_ASSERT(node->type == NodeType::eNull);
        CARB_ASSERT(node->len == 0);

        if (len == 0)
        {
            *dest = nullptr;
            node->type = type;
            node->len = len;
            return true;
        }

        *dest = static_cast<T*>(m_alloc->alloc(len * sizeof(**dest)));
        if (*dest == nullptr)
        {
            CARB_LOG_ERROR("allocator ran out of memory (requested %zu bytes)", len * sizeof(**dest));
            return false;
        }

        CARB_ASSERT((uintptr_t(*dest) & (alignof(bool) - 1)) == 0);

        node->type = type;
        node->len = len;
        memcpy(*dest, data, len * sizeof(*data));
        return true;
    }

    Allocator* m_alloc;
};

} // namespace structuredlog
} // namespace omni