diff --git a/.gitignore b/.gitignore index 3e759b7..1899660 100644 --- a/.gitignore +++ b/.gitignore @@ -1,330 +1,2 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ -**/Properties/launchSettings.json - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ +build +.vscode \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d8dc293 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "external/Catch2"] + path = external/Catch2 + url = https://github.com/catchorg/Catch2 diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..db54c9f --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.12) + +project(jsonbuilder) + +# Include sub-projects. + +add_subdirectory(src) + +# Only include testing stuff if we are the top level +if (${CMAKE_PROJECT_NAME} STREQUAL ${PROJECT_NAME}) + enable_testing() + + if (NOT TARGET Catch2::Catch2) + list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/external/Catch2/contrib") + add_subdirectory(external/Catch2) + endif () + + add_subdirectory(test) +endif () diff --git a/DEPENDENCIES b/DEPENDENCIES new file mode 100644 index 0000000..de16ea9 --- /dev/null +++ b/DEPENDENCIES @@ -0,0 +1 @@ +uuid-dev \ No newline at end of file diff --git a/TODO b/TODO new file mode 100644 index 0000000..126fd34 --- /dev/null +++ b/TODO @@ -0,0 +1,2 @@ +- Fix comments +- Consider a CMake export config file diff --git a/cmake/modules/Finduuid.cmake b/cmake/modules/Finduuid.cmake new file mode 100644 index 0000000..1f0b637 --- /dev/null +++ b/cmake/modules/Finduuid.cmake @@ -0,0 +1,25 @@ +# +# Input variables: +# UUID_PREFIX +# +# Output variables +# UUID_FOUND +# UUID_LIBRARIES +# UUID_INCLUDE_DIRS +# + +if (UUID_INCLUDE_DIRS AND UUID_LIBRARIES) + set(UUID_FIND_QUIETLY TRUE) +else () + find_path(UUID_INCLUDE_DIRS NAMES uuid/uuid.h HINTS ${UUID_PREFIX}) + find_library(UUID_LIBRARIES NAMES uuid HINTS ${UUID_PREFIX}) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(UUID DEFAULT_MSG UUID_LIBRARIES UUID_INCLUDE_DIRS) + + mark_as_advanced(UUID_LIBRARIES UUID_INCLUDE_DIRS) +endif() + +add_library(uuid INTERFACE) +target_include_directories(uuid INTERFACE ${UUID_INCLUDE_DIRS}) +target_link_libraries(uuid INTERFACE ${UUID_LIBRARIES}) diff --git a/external/Catch2 b/external/Catch2 new file mode 160000 index 0000000..979bbf0 --- /dev/null +++ b/external/Catch2 @@ -0,0 +1 @@ +Subproject commit 979bbf03bb00bc55ca09783791b5091a2247df68 diff --git a/include/jsonbuilder/JsonBuilder.h b/include/jsonbuilder/JsonBuilder.h new file mode 100644 index 0000000..b5b6c5d --- /dev/null +++ b/include/jsonbuilder/JsonBuilder.h @@ -0,0 +1,1593 @@ +/* +JsonBuilder stores data values of arbitrary type in an optimized tree +structure (i.e. values may have children). The JsonBuilder type is suitable for +storing any kind of hierarchical data. It was originally designed as a data +structure for building up a payload that will be serialized into a JSON string, +though it can be used in other scenarios that require similar organization. + +Summary: + +- enum JsonType + Indicates the type of a value that is stored in a JsonBuilder. +- class JsonValue + Interface to a value that is stored in a JsonBuilder. +- class JsonBuilder + Object that stores a tree of values. +- class JsonImplementType + Traits type used to extend JsonBuilder to work with a user-defined type. + +JsonBuilder design decisions: + +- Follows STL container design conventions (e.g. begin() and end() methods). +- Optimized for building up, lightly-manipulating, and rendering payloads. +- Less-optimized for searching through payloads, i.e. items are not indexed. + For example, finding a value with a particular name means iterating through + all of the parent's children and checking each child for a matching name. +- Nodes in the tree are one of two types. Simple nodes contain typed values + (type tag plus binary data) but have no children. Complex nodes contain no + data but may have child nodes. +- Simple nodes support arbitrary binary payloads. Any type of data can be + stored with any type tag, and the JsonBuilder itself performs no validation + of the stored data (though it does provide built-in support for creating + or accessing the contents of nodes of certain built-in types). The + JsonRenderer class and the convenience methods have built-in support for data + stored as signed-integer (1, 2, 4, or 8 bytes), unsigned-integer (1, 2, 4, or + 8 bytes), floating-point (4 or 8 bytes), boolean (true/false), null, time + (FILETIME), UUID (GUID), and string (utf-16). The convenience methods and the + renderer can be extended if other types are needed. +- Complex nodes come in two types: Object and Array. The JsonBuilder itself + makes no distinction between these two types (they have identical behavior) + but it is intended that the Object type contain named values (i.e. it is a + dictionary with string keys) and that the Array type contain anonymous values + (i.e. it is a list). +- Value name limited to 16M utf-16 characters per value. +- Value data limited to 3GB per value. +- Names are stored as utf-16. +- Memory usage for object and array values is (in bytes): + 20 + sizeof(name) + padding to a multiple of 4. +- Memory usage for all other values is (in bytes): + 12 + sizeof(name) + sizeof(data) + padding to multiple of 4. +- Total storage limited to 16GB per JsonBuilder (or available VA space). + +Error handling: + + Throw std::exception variants +*/ + +#pragma once +#include // std::min +#include +#include +#include +#include // isfinite, sqrt +#include // std::invalid_argument +#include + +#include + +namespace jsonbuilder +{ +struct UuidStruct +{ + uuid_t Data; +}; + +// Forward declarations + +class JsonValue; +class JsonBuilder; +template +class JsonImplementType; + +// Internal implementation details + +namespace JsonInternal +{ +/* +PodVector: + +Very simple vector class. For POD types only. Has the following performance +advantages over std::vector: + +- This vector uses HeapReAlloc to grow its buffer. In cases where the + HeapReAlloc can grow in-place, this avoids the overhead of allocating a + new buffer, copying the data to the new buffer, and freeing the old + buffer. +- This vector does not initialize data on resize unless specifically requested. + +In benchmarks, using PodVector instead of VC2013's std::vector improves +JsonBuilder and JsonRenderer performance by about 10%. + +In addition, using our own vector avoids creating a hard dependency on STL, +which can make it easier for customers to consume this library. +*/ + +class PodVectorBase +{ + protected: + typedef unsigned size_type; + + /* + assert(index < currentSize) + */ + static void CheckOffset(size_type index, size_type currentSize) throw(); + + /* + assert(p1 <= p2 <= p3) + */ + static void + CheckRange(void const* p1, void const* p2, void const* p3) throw(); + + /* + return checked(a + b) + */ + static size_type CheckedAdd( + size_type a, + size_type b); // may throw length_error + + /* + memcpy(pDest, pSource, cb) + */ + static void InitData(void* pDest, void const* pSource, size_t cb) throw(); + + /* + Returns a value newCapacity such that minCapacity <= newCapacity <= + maxCapacity, chosen according to a vector growth policy. The current policy + is: + - If maxCapacity < minCapacity or maxCapacity < 15, assert. + - If minCapacity < 15, return 15. + - Let cap = the smallest value 2^N-1 that is greater than or equal to + minCapacity. + - If cap < maxCapacity, return cap. Else return maxCapacity. + */ + static size_type GetNewCapacity(size_type minCapacity, size_type maxCapacity); + + /* + Calls HeapAlloc or HeapReAlloc. If allocation fails, throw bad_alloc. + */ + static void* Reallocate( + void* pb, + size_t cb, + bool zeroInitializeMemory = false); // may throw bad_alloc, length_error + + /* + Calls HeapFree. + */ + static void Deallocate(void* pb) throw(); +}; + +template +class PodVector : private PodVectorBase +{ + static size_type const + m_maxSize = ~size_t(0) / sizeof(T) > size_type(~size_type(1)) ? + size_type(~size_type(1)) : + ~size_t(0) / sizeof(T); + static_assert( + m_maxSize < size_type(m_maxSize + 1), + "Bad calculation of m_maxSize (1)"); + static_assert( + m_maxSize * sizeof(T) / sizeof(T) == m_maxSize, + "Bad calculation of m_maxSize (2)"); + + T* m_data; + size_type m_size; + size_type m_capacity; + bool m_zeroInitializeMemory; + + public: + using PodVectorBase::size_type; + + ~PodVector() throw() { Deallocate(m_data); } + + PodVector() throw() + : m_data(nullptr), m_size(0), m_capacity(0), m_zeroInitializeMemory(false) + { + return; + } + + PodVector(PodVector&& other) throw() + : m_data(other.m_data) + , m_size(other.m_size) + , m_capacity(other.m_capacity) + , m_zeroInitializeMemory(other.m_zeroInitializeMemory) + { + other.m_data = nullptr; + other.m_size = 0; + other.m_capacity = 0; + other.m_zeroInitializeMemory = false; + } + + PodVector(PodVector const& other) // may throw bad_alloc + : m_data(nullptr) + , m_size(other.m_size) + , m_capacity(other.m_size) + , m_zeroInitializeMemory(other.m_zeroInitializeMemory) + { + if (m_size != 0) + { + auto cb = m_size * sizeof(T); + m_data = + static_cast(Reallocate(nullptr, cb, m_zeroInitializeMemory)); + InitData(m_data, other.m_data, cb); + } + } + + PodVector( + T const* data, + size_type size) // may throw bad_alloc + : m_data(nullptr) + , m_size(size) + , m_capacity(size) + , m_zeroInitializeMemory(false) + { + if (m_size != 0) + { + auto cb = m_size * sizeof(T); + m_data = + static_cast(Reallocate(nullptr, cb, m_zeroInitializeMemory)); + InitData(m_data, data, cb); + } + } + + PodVector& operator=(PodVector&& other) throw() + { + PodVector(static_cast(other)).swap(*this); + return *this; + } + + PodVector& operator=(PodVector const& other) // may throw bad_alloc + { + PodVector(other).swap(*this); + return *this; + } + + static constexpr size_type max_size() throw() { return m_maxSize; } + + size_type size() const throw() { return m_size; } + + bool empty() const throw() { return m_size == 0; } + + size_type capacity() const throw() { return m_capacity; } + + T const* data() const throw() { return m_data; } + + T* data() throw() { return m_data; } + + T const& operator[](unsigned i) const throw() + { + CheckOffset(i, m_size); + return m_data[i]; + } + + T& operator[](unsigned i) throw() + { + CheckOffset(i, m_size); + return m_data[i]; + } + + void clear() { m_size = 0; } + + void push_back(T const& val) // may throw bad_alloc, length_error + { + if (m_size == m_capacity) + { + Grow(); + } + m_data[m_size++] = val; + } + + void append( + T const* pItems, + size_type cItems) // may throw bad_alloc, length_error + { + if (cItems > m_capacity - m_size) + { + GrowBy(cItems); + } + + InitData(m_data + m_size, pItems, cItems * sizeof(T)); + m_size += cItems; + } + + void append( + size_type cCopies, + T const& val) // may throw bad_alloc, length_error + { + if (cCopies > m_capacity - m_size) + { + GrowBy(cCopies); + } + + auto p = m_data + m_size; + for (size_type i = 0; i != cCopies; i += 1) + { + p[i] = val; + } + + m_size += cCopies; + } + + void reserve(size_type minCapacity) // may throw bad_alloc, length_error + { + if (m_capacity < minCapacity) + { + GrowTo(minCapacity); + } + } + + /* + NOTE: new items are uninitialized, unless m_zeroInitializeMemory is set + */ + void resize(size_type newSize) // may throw bad_alloc, length_error + { + reserve(newSize); + m_size = newSize; + } + + void swap(PodVector& other) throw() + { + auto const d = m_data; + m_data = other.m_data; + other.m_data = d; + + auto const s = m_size; + m_size = other.m_size; + other.m_size = s; + + auto const c = m_capacity; + m_capacity = other.m_capacity; + other.m_capacity = c; + + auto const z = m_zeroInitializeMemory; + m_zeroInitializeMemory = other.m_zeroInitializeMemory; + other.m_zeroInitializeMemory = z; + } + + void EnableZeroInitializeMemory() { m_zeroInitializeMemory = true; } + + /* + Ensures capacity for at least cItems additional elements, then returns + a pointer to the current end of the vector. Caller can write to this + pointer, then call SetEndPointer to include the newly-written data in + the vector. + + auto p = v.GetAppendPointer(10); + p[0] = ...; // p has room for 10 items. + v.SetEndPointer(p + 10); // Include the newly-written items in vector. + */ + T* GetAppendPointer(unsigned cItems) // may throw bad_alloc, length_error + { + if (cItems > m_capacity - m_size) + { + GrowBy(cItems); + } + return m_data + m_size; + } + + void SetEndPointer(T* pNewEnd) throw() + { + CheckRange(m_data, pNewEnd, m_data + m_capacity); + m_size = static_cast(pNewEnd - m_data); + } + + private: + void Grow() // may throw bad_alloc, length_error + { + GrowTo(m_capacity + 1); + } + + void GrowBy(size_type cItems) // may throw bad_alloc, length_error + { + GrowTo(CheckedAdd(m_size, cItems)); + } + + void GrowTo(size_type minCapacity) // may throw bad_alloc, length_error + { + size_type const newCapacity = GetNewCapacity(minCapacity, m_maxSize); + m_data = static_cast(Reallocate( + m_data, newCapacity * sizeof(T), m_zeroInitializeMemory)); + m_capacity = newCapacity; + } +}; +} +// namespace JsonInternal + +// JsonType + +/* +The built-in types supported by JsonBuilder and JsonRenderer. +Note that type values larger than 255 will not work correctly. +Note that JsonBuilder itself is actually only aware of types JsonHidden, +JsonArray and JsonObject. All of the other types are ignored by JsonBuilder. +There is no enforcement of any rules such as "names are not allowed on array +elements" or "names must be unique within an object". The JsonBuilder just +stores the names and data as binary blobs. + +Additional simple types can be used with JsonBuilder as necessary. (It is not +possible to add new complex types, i.e. user-defined types cannot have child +nodes.) To add new types in your own project: + +- Define JsonType constants for your types. Start numbering at 1. For example: + const JsonType JsonMyStuff = static_cast(1); +- Define a class that is derived from JsonRenderer. Override the RenderCustom + method to render values of your types. Use your derived class instead of + using JsonRenderer. +- If you want jsonBuilder.push_back(..., data), jsonValue.GetUnchecked(), + and jsonValue.ConvertTo to work for your types, + define specializations of JsonImplementType for each of your types. +*/ +enum JsonType : uint32_t +{ + // Numbering for custom types should start at 1. Custom types never have + // children. Numbering for custom types must not exceed 200. + JsonTypeReserved = 201, + JsonTypeBuiltIn = 244, + JsonUtf8, // No children. Data = utf-8 string. + JsonUInt, // No children. Data = uint (1, 2, 4, or 8 bytes). + JsonInt, // No children. Data = int (1, 2, 4, or 8 bytes). + JsonFloat, // No children. Data = float (4 or 8 bytes). + JsonBool, // No children. Data = bool (1 or 4 bytes). + JsonTime, // No children. Data = uint64 (number of 100ns intervals since + // 1601-01-01T00:00:00Z, i.e. Win32 FILETIME). + JsonUuid, // No children. Data = UUID (16 bytes universally unique + // identifier, i.e. Win32 GUID). + JsonNull, // No children. Data = void (0 bytes). + JsonHidden, // An erased or sentinel value. No data. (Calling Data() on a + // hidden value is an error.) + JsonArray, // Anonymous children. No data. (Calling Data() on an array + // value is an error.) + JsonObject, // Named children. No data. (Calling Data() on an object value + // is an error.) +}; + +// JsonValue + +/* +For internal use only. +Used for hidden erased or sentinel nodes, which do not need m_cbData. +*/ +class JsonValueBase +{ + friend class JsonBuilder; + friend class JsonValue; + typedef uint32_t Index; + + Index m_nextIndex; // The index of the "next" node. (Nodes form a + // singly-linked list). + uint32_t m_cchName : 24; + JsonType m_type : 8; +}; + +/* +Each value in a JsonBuilder is represented as a JsonValue. Each JsonValue +stores: + +- Type - a value from the JsonType enumeration, or a custom type. +- Name - utf-16 string, up to 16M characters. +- Data - binary blob, up to 3GB. + +Note that Object, Array, and hidden values do not contain data. +- It is an error to use a non-zero value for cbData when creating an +array/object. +- It is an error to call the Data() method on an array/object/hidden value. +*/ +class JsonValue : private JsonValueBase +{ + friend class JsonBuilder; + typedef unsigned StoragePod; + + union + { + uint32_t m_cbData; // Normal node only (not present for sentinel, + // array, or object) + Index m_lastChildIndex; // Array/Object node only (not present for + // sentinel or normal) + }; + + /* + Implementation details: + + The JsonBuilder stores a vector, which is just storage for + raw data. StoragePod could just be a byte, but storing at a larger + granularity gives benefits, especially in terms of data alignment. At + present StoragePod is uint32, so data is 32-bit aligned. This gives + perfect alignment for everything except float64 and int64, at a price of + up to 5 bytes per value wasted as padding (up to 2 bytes of padding after + the name, and up to 3 bytes of padding after the data). Within the vector, + information is arranged into nodes. The location within the vector where + the node begins is used as the node's index, e.g. + JsonValue& node = reinterpret_cast(storageVector[index]); + + Nodes are arranged into a tree. Only Array and Object nodes can be + interior nodes of the tree. All other nodes are leaf nodes. When an array + or object node is created, its first child is also created immediately, + directly following the Array or Object node in the storage vector. The + first child is always a hidden sentinel node. Since the first child always + immediately follows its parent array or object node, we can easily compute + the index of the first child as: + + parent.FirstChildIndex = parent.Index + DATA_OFFSET(parent.cchName). + + Since sentinel nodes are always hidden and never have data, we omit the + m_cbData member, saving 4 bytes per array/object. + + Since Array and Object nodes never have data, we reuse the m_cbData field + to store the index of the last child. If FirstChildIndex == LastChildIndex, + the parent has no visible children (its only child is the hidden sentinel). + + Nodes are arranged into a singly-linked list via the m_nextIndex member. + The end of the list is indicated by m_nextIndex == 0. All children of the + same parent are together in the list, so iteration from Node.FirstChild to + Node.LastChild covers all of the children of Node. + + If it exists, the node for the root object is stored at index 0, and its + associated hidden first child is always at index 3 (root.cchName == 0, so + DATA_OFFSET always returns 3 for root). Note that the root object is + created lazily, so it is possible for the root object to not actually be + stored (i.e. storageVector.size() might be 0). The root object is created + the first time any child node is created. The root object serves as both + the head and the tail of the singly-linked list of nodes. + + Nodes are erased by marking them as hidden. They are not removed from the + storage vector and are not removed from the linked list. (Changing this + would require several other changes such as adding a parentIndex and a + prevIndex to each node.) Hidden nodes are skipped during iterator + traversal. As long as there are no erased nodes, all iterator operations + are essentially O(1). However, iteration must skip hidden/erased nodes, + so begin(), begin(itParent), end(itParent), and operator++ operations can + potentially become O(N) if there are a large number of erased or childless + nodes. + + + + Note that methods in this class must never return a hidden node -- they + must return the first non-hidden node after the node they might have + otherwise returned. Because the external interface abstracts these hidden + nodes away, external implementations of count() or find() must be slightly + sub-optimal (they always have to find end(itParent), which is the first + non-hidden node AFTER itParent's last child). Internal implementations of + count(), find(), splice(), etc. do take advantage of the ability to + reference hidden nodes to become slightly more efficient -- they stop + iteration AT itParent's last child (hidden or not) instead of stopping + AFTER itParent's last child. + + Normal node format: + 0: m_nextIndex (4 bytes) + 4: m_cchName (3 bytes) + 7: m_type (1 byte) + 8: m_cbData (4 bytes) + 12: Name (cchName * 2 bytes) + xx: Padding (to a multiple of StoragePod size) + xx: Data (cbValue bytes) + xx: Padding (to a multiple of StoragePod size) + + Composite node format (array or object): + 0: m_nextIndex (4 bytes) + 4: m_cchName (3 bytes) + 7: m_type (1 byte) // JsonObject, JsonArray. + 8: m_lastChildIndex (4 bytes) + 12: Name (cchName * 2 bytes) + xx: Padding (to a multiple of StoragePod size) + xx: FirstChild (8 bytes) - a sentinel node. + + Hidden/sentinel node format: + 0: m_nextIndex (4 bytes) + 4: m_cchName (3 bytes) + 7: m_type (1 byte) // JsonHidden. + */ + + public: + JsonValue(JsonValue const&) = delete; + void operator=(JsonValue const&) = delete; + + /* + Gets the type of the value. + */ + JsonType Type() const throw(); + + /* + Gets the name of the value. + */ + std::string_view Name() const throw(); + + /* + Gets the size of the data of the value, in bytes. + Note that hidden, object, and array values do not have data, and it is an + error to call DataSize() on a value where Type() is hidden, object, or + array. + */ + unsigned DataSize() const throw(); + + /* + Reduces the size recorded for the data. + Does not change the size of the underlying buffer. + Requires: cbNew <= DataSize(). + */ + void ReduceDataSize(unsigned cbNew) throw(); + + /* + Gets a pointer to the data of the value. + Note that hidden, object, and array values do not have data, and it is an + error to call Data() on a value where Type() is hidden, object, or array. + */ + void const* Data(unsigned* pcbData = nullptr) const throw(); + + /* + Gets a pointer to the data of the value. The data can be modified, though + the length can only be reduced, never increased. + Note that hidden, object, and array values do not have data, and it is an + error to call Data() on a value where Type() is hidden, object, or array. + */ + void* Data(unsigned* pcbData = nullptr) throw(); + + /* + Returns true if Type==Null. + */ + bool IsNull() const throw(); + + /* + Returns the value's data as a T. + Requires: this->Type() is an exact match for type T. (Checked via assert.) + Requires: this->DataSize() is a valid size for this->Type(). (Checked by + assert, returns 0 in retail if size is invalid.) + + - This method asserts that this->Type() is an exact match for T. For + example, T=int matches JsonInt but does not match JsonUInt. + - This method asserts that this->DataSize() makes sense for the target type. + For example, T=int means DataSize() must be 1, 2, 4, or 8. + - This method behaves much like a reinterpret_cast, but it takes the value's + actual data size into account. For example, if T=int and DataSize() is 1, + this method returns *reinterpret_cast(Data()). + - If running in retail and data size does not make sense for the target + type, this method returns a default value of T (typically 0). For example, + if T=int and DataSize() is 3, this method will assert and then return 0. + + T must be a supported type. Supported types include: + + - For boolean data: bool. + - For string data: utl::string_view. + - For integer data: signed and unsigned char, short, int, long, long long. + - For float data: float, double, long double. + - Any user-defined type for which JsonImplementType::GetUnchecked exists. + + Detailed semantics: + assert(this->Type() is an exact match for typeof(T)); + If DataSize() is a legal size for T, return the stored data as a T; + Else assert(false), return a default value of T (e.g. 0); + */ + template + auto GetUnchecked() const throw() + -> decltype(JsonImplementType::type>::GetUnchecked( + *(JsonValue const*) 0)) + { + return JsonImplementType::type>::GetUnchecked( + *this); + } + + /* + Attempts to convert the data to T. + If the data can be converted, sets result to the data and returns true. + Else sets result to a default value (e.g. 0) and returns false. + + For integer and float conversions, performs a range check. If source value + is out of the target type's range, returns false. + + T must be a supported type. Supported types include: + + - For boolean data: bool. + - For string data: utl::string_view. + - For integer data: signed and unsigned char, short, int, long, long long. + - For float data: float, double, long double. + - Any user-defined type for which JsonImplementType::ConvertTo exists. + */ + template< + class T, + class ConvertToDoesNotSupportThisType = + decltype(JsonImplementType::type>::ConvertTo( + *(JsonValue const*) 0, + *(T*) 0))> + bool ConvertTo(T& result) const throw() + { + return JsonImplementType::type>::ConvertTo( + *this, result); + } +}; + +// JsonIterator + +/* +Implementation of JsonBuilder::const_iterator. +*/ +class JsonConstIterator +{ + friend class JsonBuilder; // JsonBuilder needs to construct const_iterators. + typedef uint32_t Index; + + JsonBuilder const* m_pContainer; + Index m_index; + + JsonConstIterator(JsonBuilder const* pContainer, Index index) throw(); + + public: + typedef std::forward_iterator_tag iterator_category; + typedef JsonValue value_type; + typedef std::ptrdiff_t difference_type; + typedef JsonValue const* pointer; + typedef JsonValue const& reference; + + JsonConstIterator() throw(); + + bool operator==(JsonConstIterator const&) const throw(); + bool operator!=(JsonConstIterator const&) const throw(); + + reference operator*() const throw(); + pointer operator->() const throw(); + + JsonConstIterator& operator++() throw(); + JsonConstIterator operator++(int) throw(); + + /* + For iterating over this value's children. + iterator.begin() is equivalent to builder.begin(iterator). + O(1), unless hidden nodes need to be skipped. + */ + JsonConstIterator begin() const throw(); + + /* + For iterating over this value's children. + iterator.end() is equivalent to builder.end(iterator). + O(1), unless hidden nodes need to be skipped. + */ + JsonConstIterator end() const throw(); + + /* + Returns true if this iterator refers to the root object of a JsonBuilder. + Note that it is an error to dereference or ++ an iterator that references + the root object (e.g. it is an error to do jsonBuilder.end()++). + */ + bool IsRoot() const throw(); +}; + +/* +Implementation of JsonBuilder::iterator. +*/ +class JsonIterator : public JsonConstIterator +{ + friend class JsonBuilder; // JsonBuilder needs to construct iterators. + + /* + A JsonIterator is created by creating a JsonConstIterator and passing it + to the constructor of a JsonIterator. This avoids the need for + JsonConstIterator to declare JsonIterator as a friend. + */ + explicit JsonIterator(JsonConstIterator const&) throw(); + + public: + typedef JsonValue& reference; + typedef JsonValue* pointer; + + JsonIterator() throw(); + + reference operator*() const throw(); + pointer operator->() const throw(); + + JsonIterator& operator++() throw(); + JsonIterator operator++(int) throw(); + + /* + For iterating over this value's children. + iterator.begin() is equivalent to builder.begin(iterator). + O(1), unless hidden nodes need to be skipped. + */ + JsonIterator begin() const throw(); + + /* + For iterating over this value's children. + iterator.end() is equivalent to builder.end(iterator). + O(1), unless hidden nodes need to be skipped. + */ + JsonIterator end() const throw(); +}; + +// JsonBuilder + +/* +Stores data in a logical tree structure. + +The tree stores values of various types. +Object values store no data but contain any number of named child values. +Array values store no data but contain any number of unnamed child values. +All other values are leaf nodes in the tree and can store arbitrary data. + +The root of the tree is an Object. This object is implicit -- it is always +present and need not be added by the user. In methods that accept a "parent +iterator" parameter, use the end() iterator to refer to the root of the tree. +*/ +class JsonBuilder +{ + friend class JsonConstIterator; + typedef JsonValue::Index Index; + typedef JsonInternal::PodVector StorageVec; + StorageVec m_storage; + + class Validator : private JsonInternal::PodVectorBase + { + // 2-bit value. + enum ValidationState : char unsigned + { + ValNone = 0, + ValTail = 1, + ValHead = 2, + ValReached = 3, + ValMax + }; + + JsonValue::StoragePod const* const m_pStorage; + size_type const m_size; + char unsigned* const m_pMap; // 2 bits per StoragePod in m_pStorage. + + public: + ~Validator(); + + explicit Validator( + JsonValue::StoragePod const* pStorage, + size_type cStorage); // may throw bad_alloc + + void Validate(); // may throw invalid_argument + + private: + void ValidateRecurse(Index index); + + void + UpdateMap(Index index, ValidationState expectedVal, ValidationState newVal); + }; + + public: + typedef JsonValue value_type; + typedef JsonValue* pointer; + typedef JsonValue const* const_pointer; + typedef size_t size_type; + typedef int difference_type; + typedef JsonIterator iterator; + typedef JsonConstIterator const_iterator; + + /* + Initializes a new instance of the JsonBuilder class. + */ + JsonBuilder() throw(); + + /* + Initializes a new instance of the JsonBuilder class using at least the + specified initial capacity (in bytes). + */ + explicit JsonBuilder(size_type cbInitialCapacity); // may throw bad_alloc, + // length_error + + /* + Initializes a new instance of the JsonBuilder class, copying its data from + other. + */ + JsonBuilder(JsonBuilder const& other); // may throw bad_alloc + + /* + Initializes a new instance of the JsonBuilder class, moving the data from + other. + NOTE: Invalidates all iterators pointing into other. + */ + JsonBuilder(JsonBuilder&& other) throw(); + + /* + Initializes a new instance of the JsonBuilder class, copying its data from + a memory buffer. Optionally runs ValidateData (i.e. for untrusted input). + */ + JsonBuilder( + void const* pbRawData, + size_type cbRawData, + bool validateData = true); // may throw bad_alloc, length_error, + // invalid_argument + + /* + Copies data from other. + */ + JsonBuilder& operator=(JsonBuilder const& other); // may throw bad_alloc + + /* + Moves the data from other. + NOTE: Invalidates all iterators pointing into other. + */ + JsonBuilder& operator=(JsonBuilder&& other) throw(); + + /* + Throws an exception if the data in this JsonBuilder is corrupt. + Mainly for use in debugging, but this can also be used when feeding + untrusted data to JsonBuilder. + */ + void ValidateData() const; // May throw bad_alloc, invalid_argument. + + iterator begin() throw(); + const_iterator begin() const throw(); + const_iterator cbegin() const throw(); + + iterator end() throw(); + const_iterator end() const throw(); + const_iterator cend() const throw(); + + /* + Returns a pointer to the first element in the backing raw data vector. + */ + void const* buffer_data() const throw(); + + /* + Returns the size (in bytes) of the memory currently being used by this + JsonBuilder (i.e. vector.size() of the underlying storage vector). This + value is primarily intended to help in determining the appropriate value + for the initialCapacity constructor parameter. This is also the size of + the data returned by buffer_data(). + */ + size_type buffer_size() const throw(); + + /* + Returns the size (in bytes) of the memory currently allocated by this + JsonBuilder (i.e. vector.capacity() of the underlying storage vector). + This value is primarily intended to help in determining the appropriate + value for the initialCapacity constructor parameter. + */ + size_type buffer_capacity() const throw(); + + /* + Returns the maximum size (in bytes) of the memory that could be passed + to buffer_reserve or returned from buffer_size. + On 32-bit systems, this is currently slightly less than 4GB. + On 64-bit systems, this is currently slightly less than 16GB. + */ + static constexpr size_type buffer_max_size() throw() + { + return StorageVec::max_size() * sizeof(JsonValue::StoragePod); + } + + /* + If less than the specified amount of memory is currently allocated by this + JsonBuilder, allocates additional memory so that at least the specified + amount is allocated. + */ + void buffer_reserve(size_type cbMinimumCapacity); // may throw bad_alloc, + // length_error + + /* + Removes all data from this JsonBuilder and prepares it for reuse. + Keeps the currently-allocated buffer. + O(1). + */ + void clear() throw(); + + /* + Marks the specified value as Erased. Equivalent to erase(itValue, + itValue+1). Requires: itValue+1 is valid (i.e. requires that itValue != + end()). Returns: itValue+1. Implementation detail: Erased values have their + Type() changed to Hidden, and will be skipped during iteration, but they + continue to take up space in the tree. O(1), unless there are erased values + that have to be skipped to find itValue+1. + */ + iterator erase(const_iterator itValue) throw(); + + /* + Marks the specified range as Erased. + Requires: itBegin <= itEnd (i.e. repeated itBegin++ will reach itEnd). + Returns: itEnd. + Implementation detail: Erased values have their Type() changed to Hidden, + and will be skipped during iteration, but they continue to take up space + in the tree. + O(n), where n = the number of values in the range itBegin..itEnd. + */ + iterator erase(const_iterator itBegin, const_iterator itEnd) throw(); + + /* + Replaces the contents of this with the contents of other. + NOTE: Invalidates all iterators pointing into this and other. + O(1). + */ + void swap(JsonBuilder& other) throw(); + + /* + Causes all future memory allocations by this JsonBuilder to be initialized + to zero. + */ + void EnableZeroInitializeMemory() + { + m_storage.EnableZeroInitializeMemory(); + } + + /* + Navigates to a child value, starting at the root. For each name provided, + navigates to the first child with the specified name. + Each name must be a string_view or implicitly-convertible to string_view. + Returns the first match, or end() if there are no matches. + O(n), where n is the total number of children at each level. + */ + template + iterator + find(std::string_view const& firstName, NameTys const&... additionalNames) throw() + { + return iterator( + const_iterator(this, Find(0, firstName, additionalNames...))); + } + + /* + Navigates to a child value, starting at the root. For each name provided, + navigates to the first child with the specified name. + Each name must be a string_view or implicitly-convertible to string_view. + Returns the first match, or end() if there are no matches. + O(n), where n is the total number of children at each level. + */ + template + const_iterator + find(std::string_view const& firstName, NameTys const&... additionalNames) const + throw() + { + return const_iterator(this, Find(0, firstName, additionalNames...)); + } + + /* + Navigates to a child value, starting at itParent. For each name provided, + navigates to the first child with the specified name. + Each name must be a string_view or implicitly-convertible to string_view. + Returns the first match, or end() if there are no matches. + O(n), where n is the total number of children at each level. + */ + template + iterator find( + const_iterator const& itParent, + std::string_view const& firstName, + NameTys const&... additionalNames) throw() + { + ValidateIterator(itParent); + return iterator(const_iterator( + this, Find(itParent.m_index, firstName, additionalNames...))); + } + + /* + Navigates to a child value, starting at itParent. For each name provided, + navigates to the first child with the specified name. + Each name must be a string_view or implicitly-convertible to string_view. + Returns the first match, or end() if there are no matches. + O(n), where n is the total number of children at each level. + */ + template + const_iterator find( + const_iterator const& itParent, + std::string_view const& firstName, + NameTys const&... additionalNames) const throw() + { + ValidateIterator(itParent); + return const_iterator( + this, Find(itParent.m_index, firstName, additionalNames...)); + } + + /* + Returns the number of children of itParent. + O(n), where n is the number of children of itParent. + */ + unsigned count(const_iterator const& itParent) const throw(); + + /* + Returns the first child of itParent. + If itParent has no children, returns end(itParent). + O(1), unless there are erased values that have to be skipped. + */ + iterator begin(const_iterator const& itParent) throw(); + + /* + Returns the first child of itParent. + If itParent has no children, returns end(itParent). + O(1), unless there are erased values that have to be skipped. + */ + const_iterator begin(const_iterator const& itParent) const throw(); + + /* + Returns the first child of itParent. + If itParent has no children, returns cend(itParent). + O(1), unless there are erased values that have to be skipped. + */ + const_iterator cbegin(const_iterator const& itParent) const throw(); + + /* + Returns the iterator after the last child of itParent. + O(1), unless there are erased values that have to be skipped. + */ + iterator end(const_iterator const& itParent) throw(); + + /* + Returns the iterator after the last child of itParent. + O(1), unless there are erased values that have to be skipped. + */ + const_iterator end(const_iterator const& itParent) const throw(); + + /* + Returns the iterator after the last child of itParent. + O(1), unless there are erased value that have to be skipped. + */ + const_iterator cend(const_iterator const& itParent) const throw(); + + /* + Removes all children from itOldParent. + Re-inserts them as the first children of itNewParent. + Requires: itNewParent must reference an array or an object value. + O(n), where n is the number of children of itOldParent. + */ + void splice_front( + const_iterator const& itOldParent, + const_iterator const& itNewParent) throw() + { + Splice(true, itOldParent, itNewParent, PredicateTrue()); + } + + /* + Removes all children from itOldParent. + Re-inserts them as the last children of itNewParent. + Requires: itNewParent must reference an array or an object value. + O(n), where n is the number of children of itOldParent. + */ + void splice_back( + const_iterator const& itOldParent, + const_iterator const& itNewParent) throw() + { + Splice(false, itOldParent, itNewParent, PredicateTrue()); + } + + /* + Removes all children from itOldParent where pred(itChild) returns true. + Re-inserts them as the first children of itNewParent. + Requires: itNewParent must reference an array or an object value. + O(n), where n is the number of children of itOldParent. + */ + template + void splice_front( + const_iterator const& itOldParent, + const_iterator const& itNewParent, + PredTy&& pred) throw() + { + Splice(true, itOldParent, itNewParent, std::forward(pred)); + } + + /* + Removes all children from itOldParent where pred(itChild) returns true. + Re-inserts them as the last children of itNewParent. + Requires: itNewParent must reference an array or an object value. + O(n), where n is the number of children of itOldParent. + */ + template + void splice_back( + const_iterator const& itOldParent, + const_iterator const& itNewParent, + PredTy&& pred) throw() + { + Splice(false, itOldParent, itNewParent, std::forward(pred)); + } + + /* + Creates a new value. Inserts it as the first (if front is true) or last + (if front is false) child of itParent. + If pbData is null, the new value's payload is uninitialized. + Requires: itParent must reference an array or an object value. + Requires: if type is Array or Object, cbValue must be 0 and pbValue must be + null. Returns: an iterator that references the new value. O(1). + */ + iterator AddValue( + bool front, + const_iterator const& itParent, + std::string_view const& name, + JsonType type, + unsigned cbData = 0, + void const* pbData = nullptr); // may throw bad_alloc, length_error + + /* + Creates a new value. Inserts it as the first child of itParent. + If pbData is null, the new value's payload is uninitialized. + Requires: itParent must reference an array or an object value. + Requires: if type is Array or Object, cbValue must be 0 and pbValue must be + null. Returns: an iterator that references the new value. O(1). + */ + iterator push_front( + const_iterator const& itParent, + std::string_view const& name, + JsonType type, + unsigned cbData = 0, + void const* pbData = nullptr) // may throw bad_alloc, length_error + { + return AddValue(true, itParent, name, type, cbData, pbData); + } + + /* + Creates a new value. Inserts it as the first child of itParent. + If pbData is null, the new value's payload is uninitialized. + Requires: itParent must reference an array or an object value. + Requires: if type is Array or Object, cbValue must be 0 and pbValue must be + null. Returns: an iterator that references the new value. O(1). + */ + iterator push_back( + const_iterator const& itParent, + std::string_view const& name, + JsonType type, + unsigned cbData = 0, + void const* pbData = nullptr) // may throw bad_alloc, length_error + { + return AddValue(false, itParent, name, type, cbData, pbData); + } + + /* + Creates a new value. Inserts it as the first (if front is true) or last + (if front is false) child of itParent. + + Data must be a supported type. Supported types include: + + - For boolean data: bool. + - For string data: utl::string_view, wchar_t*, __wchar_t. + - For integer data: signed and unsigned char, short, int, long, long long. + - For float data: float, double, long double. + - For time data: FILETIME. + - For UUID data: GUID. + - Any user-defined type for which JsonImplementType::AddValue exists. + + We specifically do not support char because the intent is ambiguous. + + - Convert a char to __wchar_t, signed char, or unsigned char, as + appropriate. + - Convert a char* to wchar_t* (or use JsonPushBackMbcs). + + Requires: itParent must reference an array or an object value. + Returns: an iterator that references the new value. + O(1). + */ + template< + class T, + class EnableIfType = + decltype(JsonImplementType::type>::AddValue)> + iterator AddValue( + bool front, + const_iterator const& itParent, + std::string_view const& name, + T const& data) // may throw bad_alloc, length_error + { + return JsonImplementType::type>::AddValue( + *this, front, itParent, name, data); + } + + /* + Creates a new value. Inserts it as the first child of itParent. + + Data must be a supported type. Supported types include: + + - For boolean data: bool. + - For string data: utl::string_view, wchar_t*, __wchar_t. + - For integer data: signed and unsigned char, short, int, long, long long. + - For float data: float, double, long double. + - For time data: FILETIME. + - For UUID data: GUID. + - Any user-defined type for which JsonImplementType::AddValue exists. + + We specifically do not support char because the intent is ambiguous. + + - Convert a char to __wchar_t, signed char, or unsigned char, as + appropriate. + - Convert a char* to wchar_t* (or use JsonPushBackMbcs). + + Requires: itParent must reference an array or an object value. + Returns: an iterator that references the new value. + O(1). + */ + template< + class T, + class EnableIfType = + decltype(JsonImplementType::type>::AddValue)> + iterator push_front( + const_iterator const& itParent, + std::string_view const& name, + T const& data) // may throw bad_alloc, length_error + { + return JsonImplementType::type>::AddValue( + *this, true, itParent, name, data); + } + + /* + Creates a new value. Inserts it as the last child of itParent. + + Data must be a supported type. Supported types include: + + - For boolean data: bool. + - For string data: utl::string_view, wchar_t*, __wchar_t. + - For integer data: signed and unsigned char, short, int, long, long long. + - For float data: float, double, long double. + - For time data: FILETIME. + - For UUID data: GUID. + - Any user-defined type for which JsonImplementType::AddValue exists. + + We specifically do not support char because the intent is ambiguous. + + - Convert a char to __wchar_t, signed char, or unsigned char, as + appropriate. + - Convert a char* to wchar_t* (or use JsonPushBackMbcs). + + Requires: itParent must reference an array or an object value. + Returns: an iterator that references the new value. + O(1). + */ + template< + class T, + class EnableIfType = + decltype(JsonImplementType::type>::AddValue)> + iterator push_back( + const_iterator const& itParent, + std::string_view const& name, + T const& data) // may throw bad_alloc, length_error + { + return JsonImplementType::type>::AddValue( + *this, false, itParent, name, data); + } + + private: + static void AssertNotEnd(Index) throw(); + static void AssertHidden(JsonType) throw(); + void ValidateIterator(const_iterator const&) const throw(); + void ValidateParentIterator(Index) const throw(); // Note: assumes !empty() + bool CanIterateOver(const_iterator const&) const + throw(); // False if empty() or if iterator is not a parent. + JsonValue const& GetValue(Index) const throw(); + JsonValue& GetValue(Index) throw(); + Index FirstChild(Index) const throw(); // Given array/object index, return + // index of first child. + Index LastChild(Index) const throw(); // Given array/object index, return + // index of last child. + Index NextIndex(Index) const throw(); // Given index, return next + // non-hidden index. + void EnsureRootExists(); // If root object does not exist, create it. May + // throw bad_alloc. + + unsigned + FindImpl(Index parentIndex, std::string_view const& name) const throw(); + + /* + Adds an unlinked node to the storage vector. The caller must add the new + node to the list. Returns the index of the new value. + It is always ok for pbValue to be null. If so, the data will be + uninitialized. If type is JsonArray or JsonObject, cbData must be 0. + */ + Index CreateValue( + std::string_view const& name, + JsonType type, + unsigned cbData, + void const* pbData); // may throw bad_alloc, length_error + + unsigned Find(Index parentIndex) const throw() { return parentIndex; } + + template + unsigned Find( + Index parentIndex, + std::string_view const& firstName, + NameTys const&... additionalNames) const throw() + { + Index childIndex = FindImpl(parentIndex, firstName); + if (childIndex) + { + childIndex = Find(childIndex, additionalNames...); + } + return childIndex; + } + + struct PredicateTrue + { + bool operator()(const_iterator const&) const throw() { return true; } + }; + + template + void Splice( + bool front, + const_iterator const& itOldParent, + const_iterator const& itNewParent, + PredTy&& pred) throw() + { + ValidateIterator(itOldParent); + ValidateIterator(itNewParent); + + if (CanIterateOver(itOldParent)) + { + ValidateParentIterator(itNewParent.m_index); + + auto& oldParent = GetValue(itOldParent.m_index); + auto prevIndex = FirstChild(itOldParent.m_index); + auto const lastIndex = oldParent.m_lastChildIndex; + if (prevIndex != lastIndex) + { + // Make a linked list of the items that we're moving. + Index headIndex = 0; + Index* pTailIndex = &headIndex; // pTail points at the tail's + // m_nextIndex, which is used + // to store the tail's index. + + auto pPrev = &GetValue(prevIndex); + AssertNotEnd(prevIndex); + AssertHidden(pPrev->m_type); + for (;;) + { + auto currentIndex = pPrev->m_nextIndex; + auto& current = GetValue(currentIndex); + AssertNotEnd(currentIndex); // got to end() before we got + // to oldParent.LastChild + + if (current.m_type != JsonHidden && + pred(const_iterator(this, currentIndex))) + { + pPrev->m_nextIndex = current.m_nextIndex; + *pTailIndex = currentIndex; // Link current into list + // of moved items. + pTailIndex = ¤t.m_nextIndex; + *pTailIndex = currentIndex; // Store tail's index. + + if (currentIndex == lastIndex) + { + oldParent.m_lastChildIndex = prevIndex; + break; + } + } + else + { + if (currentIndex == lastIndex) + { + break; + } + + prevIndex = currentIndex; + pPrev = ¤t; + } + } + + if (headIndex != 0) + { + // Find the right place in the linked list for the moved + // nodes. Update the parent's lastChildIndex if necessary. + auto& newParent = GetValue(itNewParent.m_index); + if (front) + { + prevIndex = FirstChild(itNewParent.m_index); + if (prevIndex == newParent.m_lastChildIndex) + { + newParent.m_lastChildIndex = *pTailIndex; + } + } + else + { + prevIndex = newParent.m_lastChildIndex; + newParent.m_lastChildIndex = *pTailIndex; + } + + // Insert the moved nodes into the linked list after prev. + pPrev = &GetValue(prevIndex); + *pTailIndex = pPrev->m_nextIndex; + pPrev->m_nextIndex = headIndex; + } + } + } + } +}; + +/* +Exchanges the contents of two JsonBuilder objects. +*/ +void swap(JsonBuilder&, JsonBuilder&) throw(); + +// JsonImplementType + +/* +The JsonImplementType class is used to implement the following methods: + +- jsonBuilder.AddValue(..., data) +- jsonBuilder.push_front(..., data) +- jsonBuilder.push_back(..., data) +- jsonValue.GetUnchecked() +- jsonValue.ConvertTo(result) + +These methods will only work if a specialization of JsonImplementType +has been defined. (More specifically, there must be a specialization of +JsonImplementType>.) The JsonBuilder.h header provides +specializations for core built-in types. If you want these methods to support +other types, you can define specializations of the JsonImplementType class for +them. +*/ +template +class JsonImplementType +{ + // Default - no special support for T. + + /* + Specializations of JsonImplementType may implement any or all of the + following: + + static T GetUnchecked( // return value may be "T" or "const T&" at your + discretion. JsonValue const& value); + + static bool ConvertTo( + JsonValue const& value, + T& result); + + static JsonIterator AddValue( + JsonBuilder& builder, + bool front, + JsonConstIterator const& itParent, + utl::string_view const& name, + T data); // data parameter may be "T" or "const T&" at your discretion. + + The specialization may leave the method undefined if the given operation + should not be supported. If the specialization does provide an + implementation for a method, it should conform to the semantics of the + corresponding methods of the JsonValue or JsonBuilder class. + */ +}; + +#define JSON_DECLARE_JsonImplementType(T, getRef) \ + template<> \ + class JsonImplementType \ + { \ + public: \ + static T getRef GetUnchecked(JsonValue const& value) throw(); \ + static bool ConvertTo(JsonValue const& value, T& result) throw(); \ + static JsonIterator AddValue( \ + JsonBuilder& builder, \ + bool front, \ + JsonConstIterator const& itParent, \ + std::string_view const& name, \ + T const& data); \ + } + +#define JSON_DECLARE_JsonImplementType_AddValue(T) \ + template<> \ + class JsonImplementType \ + { \ + public: \ + static JsonIterator AddValue( \ + JsonBuilder& builder, \ + bool front, \ + JsonConstIterator const& itParent, \ + std::string_view const& name, \ + T const& data); \ + } + +JSON_DECLARE_JsonImplementType(bool, ); + +JSON_DECLARE_JsonImplementType(unsigned char, ); +JSON_DECLARE_JsonImplementType(unsigned short, ); +JSON_DECLARE_JsonImplementType(unsigned int, ); +JSON_DECLARE_JsonImplementType(unsigned long, ); +JSON_DECLARE_JsonImplementType(unsigned long long, ); + +JSON_DECLARE_JsonImplementType(signed char, ); +JSON_DECLARE_JsonImplementType(signed short, ); +JSON_DECLARE_JsonImplementType(signed int, ); +JSON_DECLARE_JsonImplementType(signed long, ); +JSON_DECLARE_JsonImplementType(signed long long, ); + +JSON_DECLARE_JsonImplementType(float, ); +JSON_DECLARE_JsonImplementType(double, ); + +JSON_DECLARE_JsonImplementType(std::chrono::system_clock::time_point, ); +JSON_DECLARE_JsonImplementType(UuidStruct, ); +JSON_DECLARE_JsonImplementType(std::string_view, ); +JSON_DECLARE_JsonImplementType_AddValue(std::string); +JSON_DECLARE_JsonImplementType_AddValue(char); + +template<> +class JsonImplementType +{ + public: + static JsonIterator AddValue( + JsonBuilder& builder, + bool front, + JsonConstIterator const& itParent, + std::string_view const& name, + char const* psz); +}; + +template<> +class JsonImplementType : public JsonImplementType +{}; + +} diff --git a/include/jsonbuilder/JsonRenderer.h b/include/jsonbuilder/JsonRenderer.h new file mode 100644 index 0000000..65b775a --- /dev/null +++ b/include/jsonbuilder/JsonRenderer.h @@ -0,0 +1,275 @@ +/* +JsonRenderer converts JsonBuilder trees into utf8 JSON text. + +Summary: +- JsonRenderer +- JsonRenderBool +- JsonRenderFalse +- JsonRenderTime +- JsonRenderFloat +- JsonRenderInt +- JsonRenderNull +- JsonRenderTrue +- JsonRenderUInt +- JsonRenderUuid +- JsonRenderUuidWithBraces +*/ + +#pragma once + +#include +#include + +namespace jsonbuilder +{ +/* +Converts JsonBuilder data into utf-8 JSON text. +Recognizes the built-in JsonType types. To support other (custom) types, +derive from JsonRenderer and override RenderCustom. +*/ +class JsonRenderer +{ + protected: + typedef JsonInternal::PodVector RenderBuffer; + typedef JsonBuilder::const_iterator iterator; + + private: + RenderBuffer m_renderBuffer; + std::string_view m_newLine; + unsigned m_indentSpaces; + unsigned m_indent; + bool m_pretty; + + public: + typedef JsonInternal::PodVector::size_type size_type; + + /* + Initializes a new instance of the JsonRenderer class. + Optionally sets the initial value of the formatting properties. + */ + explicit JsonRenderer( + bool pretty = false, + std::string_view const& newLine = "\n", + unsigned indentSpaces = 2) throw(); + + /* + Preallocates memory in the rendering buffer (increases capacity). + */ + void Reserve(size_type cb); // may throw bad_alloc, length_error + + /* + Gets the current size of the rendering buffer, in bytes. + */ + size_type Size() const throw(); + + /* + Gets the current capacity of the rendering buffer (how large it can grow + without allocating additional memory), in bytes. + */ + size_type Capacity() const throw(); + + /* + Gets a value indicating whether the output will be formatted nicely. + If true, insignificant whitespace (spaces and newlines) will be added to + improve readability by humans and to put each value on its own line. + If false, all insignificant whitespace will be omitted. + Default value is false. + */ + bool Pretty() const throw(); + + /* + Set a value indicating whether the output will be formatted nicely. + If true, insignificant whitespace (spaces and newlines) will be added to + improve readability by humans and to put each value on its own line. + If false, all insignificant whitespace will be omitted. + Default value is false. + */ + void Pretty(bool) throw(); + + /* + Gets the string that is used for newline when Pretty() is true. + Default value is "\r\n". + */ + std::string_view const& NewLine() const throw(); + + /* + Sets the string that is used for newline when Pretty() is true. + Note that the JsonRenderer will store a copy of the string_view, but it + does not make a copy of the actual string. The string passed in here must + be valid for as long as the JsonRenderer exists. + Default value is "\r\n". + */ + void NewLine(std::string_view const&) throw(); + + /* + Gets the number of spaces per indent level. Default value is 2. + */ + unsigned IndentSpaces() const throw(); + + /* + Sets the number of spaces per indent level. Default value is 2. + */ + void IndentSpaces(unsigned) throw(); + + /* + Renders the contents of the specified JsonBuilder as utf-8 JSON, starting + at the root value. + Returns a string_view with the resulting JSON string. The returned string + is nul-terminated, but the nul is not included as part of the string_view + itself, so return.data()[return.size()] == '\0'. + The returned string_view is valid until the next call to Render or until + this JsonBuilder is destroyed. + */ + std::string_view Render(JsonBuilder const& builder); // may throw + // bad_alloc, + // length_error + + /* + Renders the contents of a JsonBuilder as utf-8 JSON, starting at the + specified value. + Returns a string_view with the resulting JSON string. The returned string + is nul-terminated, but the nul is not included as part of the string_view + itself, so return.data()[return.size()] == '\0'. + The returned string_view is valid until the next call to Render or until + this JsonBuilder is destroyed. + */ + std::string_view + Render(JsonBuilder::const_iterator const& it); // may throw bad_alloc, + // length_error + + protected: + /* + Override this method to provide rendering behavior for custom value types. + The utf-8 representation of the value referenced by itValue should be + appended to the end of buffer by calling buffer.push_back with the bytes + of the utf-8 representation. + */ + virtual void RenderCustom( + RenderBuffer& buffer, + iterator const& itValue); // may throw bad_alloc, length_error + + private: + /* + Renders any value and its children by dispatching to the appropriate + subroutine. + CANNOT RENDER THE ROOT! (Don't call this if it.IsRoot() is true.) + */ + void RenderValue(iterator const& it); + + /* + Renders an object/array value and its children. + itParent must be an array or object. + Set showNames = true for object. Set showNames = false for array. + Can be called with itParent == end() to render the root. + Example output: {"ObjectName":{"ArrayName":[7]}} + */ + void RenderStructure(iterator const& itParent, bool showNames); + + /* + Renders value as floating-point. Requires that cb be 4 or 8. Data will be + interpreted as a little-endian float or double. + Example output: 123.45 + */ + void RenderFloat(double const& value); + + /* + Renders value as signed integer. Requires that cb be 1, 2, 4 or 8. Data will + be interpreted as a little-endian signed integer. + Example output: -12345 + */ + void RenderInt(long long signed const& value); + + /* + Renders value as unsigned integer. Requires that cb be 1, 2, 4 or 8. Data + will be interpreted as a little-endian unsigned integer. + Example output: 12345 + */ + void RenderUInt(long long unsigned const& value); + + /* + Renders value as time. Requires that cb be 8. Data will be interpreted as + number of 100ns intervals since 1601-01-01T00:00:00Z. + Example output: "2015-04-02T02:09:14.7927652Z". + */ + void RenderTime(std::chrono::system_clock::time_point const& value); + + /* + Renders value as UUID. Requires that cb be 16. + Example output: "CD8D0A5E-6409-4B8E-9366-B815CEF0E35D". + */ + void RenderUuid(uuid_t const& value); + + /* + Renders value as a string. Converts pch to utf-8, escapes any control + characters, and adds quotes around the result. Example output: "String\n" + */ + void RenderString(std::string_view const& value); + + /* + If pretty-printing is disabled, has no effect. + If pretty-printing is enabled, writes m_newLine followed by + (m_indent * m_indentSpaces) space characters. + */ + void RenderNewline(); +}; + +/* +Renders the given value as an unsigned decimal integer, e.g. "123". +Returns the number of characters written, not counting the nul-termination. +*/ +unsigned JsonRenderUInt(long long unsigned n, char* pBuffer) throw(); + +/* +Renders the given value as a signed decimal integer, e.g. "-123". +Returns the number of characters written, not counting the nul-termination. +*/ +unsigned JsonRenderInt(long long signed n, char* pBuffer) throw(); + +/* +Renders the given value as a signed floating-point, e.g. "-123.1", or "null" +if the value is not finite. +Returns the number of characters written, not counting the nul-termination. +*/ +unsigned JsonRenderFloat(double n, char* pBuffer) throw(); + +/* +Renders the string "true" or "false". +Returns the number of characters written, not counting the nul-termination. +(Always returns 4 or 5.) +*/ +unsigned JsonRenderBool(bool b, char* pBuffer) throw(); + +/* +Renders the string "null". +Returns the number of characters written, not counting the nul-termination. +(Always returns 4.) +*/ +unsigned JsonRenderNull(char* pBuffer) throw(); + +/* +Renders the given FILETIME value (uint64) as an ISO 8601 string, e.g. +"2015-04-02T02:09:14.7927652Z". +Returns the number of characters written, not counting the nul-termination. +(Always returns 28.) +*/ +unsigned JsonRenderTime( + std::chrono::system_clock::time_point const& ft, + char* pBuffer) throw(); + +/* +Renders the given GUID value as a string in uppercase without braces, e.g. +"CD8D0A5E-6409-4B8E-9366-B815CEF0E35D". +Returns the number of characters written, not counting the nul-termination. +(Always returns 36.) +*/ +unsigned JsonRenderUuid(uuid_t const& g, char* pBuffer) throw(); + +/* +Renders the given GUID value as a string in uppercase with braces, e.g. +"{CD8D0A5E-6409-4B8E-9366-B815CEF0E35D}". +Returns the number of characters written, not counting the nul-termination. +(Always returns 38.) +*/ +unsigned JsonRenderUuidWithBraces(uuid_t const& g, char* pBuffer) throw(); + +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..d5e4d59 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.12) + +list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake/modules) +find_package(uuid REQUIRED) + +set(JB_PrivateHeaders) +set(JB_SourceFiles JsonBuilder.cpp JsonRenderer.cpp PodVector.cpp) + +set(PublicHeaderSrcPrefix ${PROJECT_SOURCE_DIR}/include/jsonbuilder) +set(JB_PublicHeaders ${PublicHeaderSrcPrefix}/JsonBuilder.h ${PublicHeaderSrcPrefix}/JsonRenderer.h) + +# Add source to this project's executable. +add_library(jsonbuilder ${JB_SourceFiles} ${JB_PrivateHeaders} ${JB_PublicHeaders}) +target_include_directories(jsonbuilder PUBLIC ${PROJECT_SOURCE_DIR}/include) +target_link_libraries(jsonbuilder PUBLIC uuid) + +set_property(TARGET jsonbuilder PROPERTY CXX_STANDARD 17) +set_property(TARGET jsonbuilder PROPERTY POSITION_INDEPENDENT_CODE ON) + +install(TARGETS jsonbuilder DESTINATION lib) +install(FILES ${JB_PublicHeaders} DESTINATION include/jsonbuilder) diff --git a/src/JsonBuilder.cpp b/src/JsonBuilder.cpp new file mode 100644 index 0000000..afbfc90 --- /dev/null +++ b/src/JsonBuilder.cpp @@ -0,0 +1,1409 @@ +#include "jsonbuilder/JsonBuilder.h" + +#include + +#define StorageSize sizeof(JsonValue::StoragePod) +#define DataMax 0xf0000000 + +/* +Computes the difference between a value's index and the index at which its +data begins. Used to determine the DataIndex for a value: +value.DataIndex = value.Index + DATA_OFFSET(value.cchName). +*/ +#define DATA_OFFSET(cchName) \ + (((cchName) + sizeof(JsonValue) + (StorageSize - 1)) / StorageSize) + +#define IS_SPECIAL_TYPE(type) (JsonHidden <= (type)) +#define IS_NORMAL_TYPE(type) ((type) < JsonHidden) +#define IS_COMPOSITE_TYPE(type) (JsonArray <= (type)) + +// JsonValue + +namespace jsonbuilder +{ +// Verify that our conditions like (type >= JsonArray) will work. +static_assert(JsonHidden == 253, "Incorrect enum numbering for JsonHidden"); +static_assert(JsonArray == 254, "Incorrect enum numbering for JsonArray"); +static_assert(JsonObject == 255, "Incorrect enum numbering for JsonObject"); + +static_assert( + sizeof(unsigned) >= 4, + "JsonValue assumes that unsigned is at least 32 bits"); +static_assert(sizeof(JsonValueBase) == 8, "JsonValueBase changed size"); +static_assert(sizeof(JsonValue) == 12, "JsonValue changed size"); + +JsonType JsonValue::Type() const throw() +{ + return static_cast(m_type); +} + +std::string_view JsonValue::Name() const throw() +{ + return std::string_view(reinterpret_cast(this + 1), m_cchName); +} + +void const* JsonValue::Data(unsigned* pcbData) const throw() +{ + return const_cast(this)->Data(pcbData); +} + +void* JsonValue::Data(unsigned* pcbData) throw() +{ + assert(!IS_SPECIAL_TYPE(m_type)); // Can't call Data() on hidden, + // object, or array values. + if (pcbData != nullptr) + { + *pcbData = m_cbData; + } + + return reinterpret_cast(this) + DATA_OFFSET(m_cchName); +} + +unsigned JsonValue::DataSize() const throw() +{ + assert(!IS_SPECIAL_TYPE(m_type)); // Can't call DataSize() on hidden, + // object, or array values. + return m_cbData; +} + +void JsonValue::ReduceDataSize(unsigned cbNew) throw() +{ + if (IS_SPECIAL_TYPE(m_type) || cbNew > m_cbData) + { + assert(!"JsonBuilder: invalid use of ReduceDataSize()."); + std::terminate(); + } + + m_cbData = cbNew; +} + +bool JsonValue::IsNull() const throw() +{ + return m_type == JsonNull; +} + +// JsonConstIterator + +JsonConstIterator::JsonConstIterator() throw() : m_pContainer(), m_index() +{ + return; +} + +JsonConstIterator::JsonConstIterator(JsonBuilder const* pContainer, Index index) throw() + : m_pContainer(pContainer), m_index(index) +{ + return; +} + +bool JsonConstIterator::operator==(JsonConstIterator const& other) const throw() +{ + assert(m_pContainer == other.m_pContainer); + return m_index == other.m_index; +} + +bool JsonConstIterator::operator!=(JsonConstIterator const& other) const throw() +{ + assert(m_pContainer == other.m_pContainer); + return m_index != other.m_index; +} + +JsonConstIterator::reference JsonConstIterator::operator*() const throw() +{ + m_pContainer->AssertNotEnd(m_index); // Do not dereference the end() + // iterator. + return m_pContainer->GetValue(m_index); +} + +JsonConstIterator::pointer JsonConstIterator::operator->() const throw() +{ + m_pContainer->AssertNotEnd(m_index); // Do not dereference the end() + // iterator. + return &m_pContainer->GetValue(m_index); +} + +JsonConstIterator& JsonConstIterator::operator++() throw() +{ + m_index = m_pContainer->NextIndex(m_index); // Implicitly asserts !end(). + return *this; +} + +JsonConstIterator JsonConstIterator::operator++(int) throw() +{ + auto old = *this; + m_index = m_pContainer->NextIndex(m_index); // Implicitly asserts !end(). + return old; +} + +JsonConstIterator JsonConstIterator::begin() const throw() +{ + return m_pContainer->begin(*this); +} + +JsonConstIterator JsonConstIterator::end() const throw() +{ + return m_pContainer->end(*this); +} + +bool JsonConstIterator::IsRoot() const throw() +{ + return m_index == 0; +} + +// JsonIterator + +JsonIterator::JsonIterator() throw() : JsonConstIterator() +{ + return; +} + +JsonIterator::JsonIterator(JsonConstIterator const& other) throw() + : JsonConstIterator(other) +{ + return; +} + +JsonIterator::reference JsonIterator::operator*() const throw() +{ + return const_cast(JsonConstIterator::operator*()); +} + +JsonIterator::pointer JsonIterator::operator->() const throw() +{ + return const_cast(JsonConstIterator::operator->()); +} + +JsonIterator& JsonIterator::operator++() throw() +{ + JsonConstIterator::operator++(); + return *this; +} + +JsonIterator JsonIterator::operator++(int) throw() +{ + JsonIterator old(*this); + JsonConstIterator::operator++(); + return old; +} + +JsonIterator JsonIterator::begin() const throw() +{ + return JsonIterator(JsonConstIterator::begin()); +} + +JsonIterator JsonIterator::end() const throw() +{ + return JsonIterator(JsonConstIterator::end()); +} + +// JsonBuilder::Validator + +constexpr unsigned char MapBits = 2; +constexpr unsigned char MapMask = (1 << MapBits) - 1; +constexpr unsigned char MapPerByte = 8 / MapBits; +#define MapSize(cStorage) ((cStorage) / MapPerByte + 1u) + +JsonBuilder::Validator::~Validator() +{ + Deallocate(m_pMap); +} + +JsonBuilder::Validator::Validator( + JsonValue::StoragePod const* pStorage, + size_type cStorage) + : m_pStorage(pStorage) + , m_size(cStorage) + , m_pMap(static_cast(Reallocate(nullptr, MapSize(cStorage)))) +{ + static_assert( + ValMax <= (1 << MapBits), "Too many ValidationStates for MapBits"); + assert(m_size != 0); // No need to validate an empty builder. + return; +} + +void JsonBuilder::Validator::Validate() +{ + memset(m_pMap, 0, MapSize(m_size)); + + // Traverse the linked list, starting at head. + // Ensure that items fit in storage without overlap. + // Ensure that there are no loops. + // Mark all valid heads. + Index index = 0; + do + { + static_assert( + sizeof(JsonValueBase) % StorageSize == 0, + "JsonValueBase not a multiple of StorageSize"); + static_assert( + sizeof(JsonValue) % StorageSize == 0, + "JsonValue not a multiple of StorageSize"); + + // Note: don't dereference pValue until the appropriate part has been + // validated. + JsonValue const* const pValue = + reinterpret_cast(m_pStorage + index); + + // Mark start of JsonValueBase as head. + UpdateMap(index, ValNone, ValHead); + + // Mark remainder of JsonValueBase as tail. + Index const baseEnd = sizeof(JsonValueBase) / StorageSize; + for (Index i = 1; i != baseEnd; i++) + { + UpdateMap(index + i, ValNone, ValTail); + } + + // Now safe to dereference: m_nextIndex, m_cchName, m_type. + + if (pValue->m_type != JsonHidden) + { + // Mark m_cbData/m_lastChildIndex, Name as tail. + Index const nameEnd = DATA_OFFSET(pValue->m_cchName); + for (Index i = baseEnd; i != nameEnd; i++) + { + UpdateMap(index + i, ValNone, ValTail); + } + + // Now safe to dereference: m_cbData/m_lastChildIndex, Name. + + if (IS_NORMAL_TYPE(pValue->m_type)) + { + if (pValue->m_cbData > DataMax) + { + throw std::invalid_argument("JsonBuilder - corrupt data"); + } + + // Mark Data as tail. + Index const dataEnd = nameEnd + + (pValue->m_cbData + StorageSize - 1) / StorageSize; + for (Index i = nameEnd; i != dataEnd; i++) + { + UpdateMap(index + i, ValNone, ValTail); + } + } + } + + index = pValue->m_nextIndex; + } while (index != 0); + + // Validate root. + UpdateMap(0, ValHead, ValReached); + if (reinterpret_cast(m_pStorage)->m_cchName != 0 || + reinterpret_cast(m_pStorage)->m_type != JsonObject) + { + throw std::invalid_argument("JsonBuilder - corrupt data"); + } + + // Traverse the tree, starting at root. + // Ensure that all reachable indexes are valid heads. + // Ensure that there are no child->parent loops (no head reached more than + // once). + ValidateRecurse(0); + + return; +} + +void JsonBuilder::Validator::ValidateRecurse(Index parent) +{ + JsonValue const* const pParent = + reinterpret_cast(m_pStorage + parent); + + // Validate first child (always hidden/sentinel). + + Index child = parent + DATA_OFFSET(pParent->m_cchName); + UpdateMap(child, ValHead, ValReached); + JsonValue const* pChild = + reinterpret_cast(m_pStorage + child); + + if (pChild->m_type != JsonHidden) + { + throw std::invalid_argument("JsonBuilder - corrupt data"); + } + + // Validate remaining children. + + while (child != pParent->m_lastChildIndex) + { + child = pChild->m_nextIndex; + UpdateMap(child, ValHead, ValReached); + pChild = reinterpret_cast(m_pStorage + child); + + if (IS_COMPOSITE_TYPE(pChild->m_type)) + { + ValidateRecurse(child); + } + } + + return; +} + +void JsonBuilder::Validator::UpdateMap( + Index index, + ValidationState expectedVal, + ValidationState newVal) +{ + assert(newVal == (expectedVal | newVal)); + Index i = index / MapPerByte; + char shift = (index % MapPerByte) * MapBits; + if (m_size <= index || ((m_pMap[i] >> shift) & MapMask) != expectedVal) + { + throw std::invalid_argument("JsonBuilder - corrupt data"); + } + + m_pMap[i] |= newVal << shift; + assert(newVal == ((m_pMap[i] >> shift) & MapMask)); +} + +// JsonBuilder + +JsonBuilder::JsonBuilder() throw() +{ + return; +} + +JsonBuilder::JsonBuilder(size_type cbInitialCapacity) +{ + buffer_reserve(cbInitialCapacity); +} + +JsonBuilder::JsonBuilder(JsonBuilder const& other) : m_storage(other.m_storage) +{ + return; +} + +JsonBuilder::JsonBuilder(JsonBuilder&& other) throw() + : m_storage(std::move(other.m_storage)) +{ + return; +} + +JsonBuilder::JsonBuilder(void const* pbRawData, size_type cbRawData, bool validateData) + : m_storage( + static_cast(pbRawData), + static_cast(cbRawData / StorageSize)) +{ + if (cbRawData % StorageSize != 0 || + cbRawData / StorageSize > StorageVec::max_size()) + { + throw std::invalid_argument("cbRawData invalid"); + } + else if (validateData) + { + ValidateData(); + } +} + +JsonBuilder& JsonBuilder::operator=(JsonBuilder const& other) +{ + m_storage = other.m_storage; + return *this; +} + +JsonBuilder& JsonBuilder::operator=(JsonBuilder&& other) throw() +{ + m_storage = std::move(other.m_storage); + return *this; +} + +void JsonBuilder::ValidateData() const +{ + if (!m_storage.empty()) + { + Validator(m_storage.data(), m_storage.size()).Validate(); + } +} + +JsonBuilder::iterator JsonBuilder::begin() throw() +{ + return iterator(cbegin()); +} + +JsonBuilder::const_iterator JsonBuilder::begin() const throw() +{ + return cbegin(); +} + +JsonBuilder::const_iterator JsonBuilder::cbegin() const throw() +{ + Index index = 0; + if (!m_storage.empty()) + { + // Return index of first non-hidden node after root. + index = GetValue(0).m_nextIndex; + for (;;) + { + auto& value = GetValue(index); + if (value.m_type != JsonHidden) + { + break; + } + + AssertNotEnd(index); // end() should never be hidden. + index = value.m_nextIndex; + } + } + return const_iterator(this, index); +} + +JsonBuilder::iterator JsonBuilder::end() throw() +{ + return iterator(cend()); +} + +JsonBuilder::const_iterator JsonBuilder::end() const throw() +{ + return cend(); +} + +JsonBuilder::const_iterator JsonBuilder::cend() const throw() +{ + return const_iterator(this, 0); +} + +JsonBuilder::size_type JsonBuilder::buffer_size() const throw() +{ + return m_storage.size() * StorageSize; +} + +void const* JsonBuilder::buffer_data() const throw() +{ + return m_storage.data(); +} + +JsonBuilder::size_type JsonBuilder::buffer_capacity() const throw() +{ + return m_storage.capacity() * StorageSize; +} + +void JsonBuilder::buffer_reserve(size_type cbMinimumCapacity) +{ + if (cbMinimumCapacity > StorageVec::max_size() * StorageSize) + { + throw std::length_error("requested capacity is too large"); + } + auto const cItems = (cbMinimumCapacity + StorageSize - 1) / StorageSize; + m_storage.reserve(static_cast(cItems)); +} + +void JsonBuilder::clear() throw() +{ + m_storage.clear(); +} + +JsonBuilder::iterator JsonBuilder::erase(const_iterator itValue) throw() +{ + ValidateIterator(itValue); + if (itValue.m_index == 0) + { + assert(!"JsonBuilder: cannot erase end()"); + std::terminate(); + } + + GetValue(itValue.m_index).m_type = JsonHidden; + return iterator(const_iterator(this, NextIndex(itValue.m_index))); +} + +JsonBuilder::iterator +JsonBuilder::erase(const_iterator itBegin, const_iterator itEnd) throw() +{ + ValidateIterator(itBegin); + ValidateIterator(itEnd); + auto index = itBegin.m_index; + while (index != itEnd.m_index) + { + if (index == 0) + { + assert(!"JsonBuilder: invalid use of ReduceDataSize()."); + std::terminate(); + } + + auto& value = GetValue(index); + value.m_type = JsonHidden; + index = value.m_nextIndex; + } + return iterator(itEnd); +} + +void JsonBuilder::swap(JsonBuilder& other) throw() +{ + m_storage.swap(other.m_storage); +} + +unsigned +JsonBuilder::FindImpl(Index parentIndex, std::string_view const& name) const + throw() +{ + Index result = 0; + if (!m_storage.empty() && IS_COMPOSITE_TYPE(GetValue(parentIndex).m_type)) + { + auto index = FirstChild(parentIndex); + auto const lastIndex = LastChild(parentIndex); + if (index != lastIndex) + { + AssertNotEnd(index); + auto& sentinelValue = GetValue(index); + assert(sentinelValue.m_type == JsonHidden); + index = sentinelValue.m_nextIndex; + for (;;) + { + AssertNotEnd(index); + auto& value = GetValue(index); + + if (value.m_type != JsonHidden && value.Name() == name) + { + result = index; + break; + } + + if (index == lastIndex) + { + break; + } + + index = value.m_nextIndex; + } + } + } + return result; +} + +unsigned JsonBuilder::count(const_iterator const& itParent) const throw() +{ + ValidateIterator(itParent); + + unsigned result = 0; + if (CanIterateOver(itParent)) + { + auto index = FirstChild(itParent.m_index); + auto const lastIndex = LastChild(itParent.m_index); + if (index != lastIndex) + { + AssertNotEnd(index); + auto& sentinelValue = GetValue(index); + assert(sentinelValue.m_type == JsonHidden); + index = sentinelValue.m_nextIndex; + for (;;) + { + AssertNotEnd(index); + auto& value = GetValue(index); + + if (value.m_type != JsonHidden) + { + ++result; + } + + if (index == lastIndex) + { + break; + } + + index = value.m_nextIndex; + } + } + } + return result; +} + +JsonBuilder::iterator JsonBuilder::begin(const_iterator const& itParent) throw() +{ + return iterator(cbegin(itParent)); +} + +JsonBuilder::const_iterator +JsonBuilder::begin(const_iterator const& itParent) const throw() +{ + return cbegin(itParent); +} + +JsonBuilder::const_iterator +JsonBuilder::cbegin(const_iterator const& itParent) const throw() +{ + ValidateIterator(itParent); + Index index = 0; + if (CanIterateOver(itParent)) + { + index = NextIndex(FirstChild(itParent.m_index)); + } + return const_iterator(this, index); +} + +JsonBuilder::iterator JsonBuilder::end(const_iterator const& itParent) throw() +{ + return iterator(cend(itParent)); +} + +JsonBuilder::const_iterator +JsonBuilder::end(const_iterator const& itParent) const throw() +{ + return cend(itParent); +} + +JsonBuilder::const_iterator +JsonBuilder::cend(const_iterator const& itParent) const throw() +{ + ValidateIterator(itParent); + Index index = 0; + if (CanIterateOver(itParent)) + { + index = NextIndex(LastChild(itParent.m_index)); + } + return const_iterator(this, index); +} + +JsonBuilder::iterator JsonBuilder::AddValue( + bool front, + const_iterator const& itParent, + std::string_view const& name, + JsonType type, + unsigned cbData, + void const* pbData) +{ + ValidateIterator(itParent); + EnsureRootExists(); + ValidateParentIterator(itParent.m_index); + Index const newIndex = CreateValue(name, type, cbData, pbData); + + // Find the right place in the linked list for the new node. + // Update the parent's lastChildIndex if necessary. + + auto& parentValue = GetValue(itParent.m_index); // GetValue(parent) must be + // AFTER the CreateValue. + Index prevIndex; // The node that the new node goes after. + if (front) + { + prevIndex = FirstChild(itParent.m_index); + if (prevIndex == parentValue.m_lastChildIndex) + { + parentValue.m_lastChildIndex = newIndex; + } + } + else + { + prevIndex = parentValue.m_lastChildIndex; + parentValue.m_lastChildIndex = newIndex; + } + + // Insert the new node into the linked list after prev. + + auto& prevValue = GetValue(prevIndex); + auto& newValue = GetValue(newIndex); + newValue.m_nextIndex = prevValue.m_nextIndex; + prevValue.m_nextIndex = newIndex; + + return iterator(const_iterator(this, newIndex)); +} + +void JsonBuilder::AssertNotEnd(Index index) throw() +{ + (void) index; // Unreferenced parameter in release builds. + assert(index != 0); +} + +void JsonBuilder::AssertHidden(JsonType type) throw() +{ + (void) type; // Unreferenced parameter in release builds. + assert(type == JsonHidden); +} + +void JsonBuilder::ValidateIterator(const_iterator const& it) const throw() +{ + assert(it.m_index == 0 || it.m_index < m_storage.size()); + + if (it.m_pContainer != this) + { + assert(!"JsonBuilder: iterator is from a different container"); + std::terminate(); + } +} + +void JsonBuilder::ValidateParentIterator(Index index) const throw() +{ + assert(!m_storage.empty()); + + if (!IS_COMPOSITE_TYPE(GetValue(index).m_type)) + { + assert(!"JsonBuilder: destination must be an array or object"); + std::terminate(); + } +} + +bool JsonBuilder::CanIterateOver(const_iterator const& it) const throw() +{ + return !m_storage.empty() && IS_COMPOSITE_TYPE(GetValue(it.m_index).m_type); +} + +JsonValue const& JsonBuilder::GetValue(Index index) const throw() +{ + return reinterpret_cast(m_storage[index]); +} + +JsonValue& JsonBuilder::GetValue(Index index) throw() +{ + return reinterpret_cast(m_storage[index]); +} + +JsonBuilder::Index JsonBuilder::FirstChild(Index index) const throw() +{ + auto cchName = reinterpret_cast(m_storage[index]).m_cchName; + return index + DATA_OFFSET(cchName); +} + +JsonBuilder::Index JsonBuilder::LastChild(Index index) const throw() +{ + return reinterpret_cast(m_storage[index]).m_lastChildIndex; +} + +JsonBuilder::Index JsonBuilder::NextIndex(Index index) const throw() +{ + assert(index < m_storage.size()); + auto pValue = reinterpret_cast(m_storage.data() + index); + for (;;) + { + assert(index != 0); // assert(it != end()) + index = pValue->m_nextIndex; + pValue = reinterpret_cast(m_storage.data() + index); + if (pValue->m_type != JsonHidden) + { + break; + } + } + return index; +} + +void JsonBuilder::EnsureRootExists() +{ + if (m_storage.empty()) + { + unsigned index; + index = CreateValue(std::string_view(), JsonObject, 0, nullptr); + assert(index == 0); + } +} + +JsonBuilder::Index JsonBuilder::CreateValue( + std::string_view const& name, + JsonType type, + unsigned cbData, + void const* pbData) +{ + assert(type <= 255); + if (name.size() > 0xffffff) + { + throw std::invalid_argument("JsonBuilder - cchName too large"); + } + + if (cbData > DataMax) + { + throw std::invalid_argument("JsonBuilder - cbValue too large"); + } + + if (IS_COMPOSITE_TYPE(type)) + { + assert(cbData == 0); + cbData = sizeof(JsonValueBase); + } + + unsigned const cchName = static_cast(name.size()); + unsigned const valueIndex = static_cast(m_storage.size()); + unsigned const dataIndex = valueIndex + DATA_OFFSET(cchName); + unsigned const newStorageSize = + dataIndex + (cbData + StorageSize - 1) / StorageSize; + auto const pOldStorageData = + reinterpret_cast(m_storage.data()); + + if (newStorageSize <= valueIndex) + { + throw std::invalid_argument("JsonBuilder - too much data"); + } + + m_storage.resize(newStorageSize); + + JsonValue* const pValue = + reinterpret_cast(m_storage.data() + valueIndex); + pValue->m_nextIndex = 0; + pValue->m_cchName = cchName; + pValue->m_type = type; + + auto pNameData = reinterpret_cast(name.data()); + auto const pNewStorageData = + reinterpret_cast(m_storage.data()); + if (pOldStorageData != pNewStorageData && pOldStorageData < pNameData && + pNameData < pOldStorageData + (valueIndex * StorageSize)) + { + // They're copying the name from within the vector, and we just resized + // out from under them. Technically, we could consider this a bug in the + // caller, but it's an easy mistake to make, easy to miss in testing, + // and hard to diagnose if it hits. Dealing with this is easy for us, so + // just fix up the problem instead of making it an error. + pNameData = pNewStorageData + (pNameData - pOldStorageData); + } + + std::memcpy(pValue + 1, pNameData, cchName); + + if (IS_COMPOSITE_TYPE(type)) + { + pValue->m_lastChildIndex = dataIndex; + + // Set up sentinel node. Insert it into the linked list. + auto pRootValue = reinterpret_cast(m_storage.data()); + auto pSentinel = + reinterpret_cast(m_storage.data() + dataIndex); + pSentinel->m_nextIndex = pRootValue->m_nextIndex; + pSentinel->m_cchName = 0; + pSentinel->m_type = JsonHidden; + pRootValue->m_nextIndex = dataIndex; + } + else + { + pValue->m_cbData = cbData; + + // Set up data. + if (pbData != nullptr) + { + auto pData = static_cast(pbData); + if (pOldStorageData != pNewStorageData && pOldStorageData < pData && + pData < pOldStorageData + (valueIndex * StorageSize)) + { + // They're copying the data from within the vector, and we just + // resized out from under them. Technically, we could consider + // this a bug in the caller, but it's an easy mistake to make, + // easy to miss in testing, and hard to diagnose when it hits. + // Dealing with this is easy for us, so just fix up the problem + // instead of making it an error. + pData = pNewStorageData + (pData - pOldStorageData); + } + + memcpy(m_storage.data() + dataIndex, pData, cbData); + } + } + + return valueIndex; +} + +void swap(JsonBuilder& a, JsonBuilder& b) throw() +{ + a.swap(b); +} + +// JsonImplementType + +/* +The macro-based GetUnchecked and ConvertTo (for f32, u8, u16, u32, i8, i16, +and i32) aren't perfectly optimal... But they're probably close enough. +*/ + +#define IMPLEMENT_AddValue(DataType, DataSize, DataPtr, ValueType) \ + JsonIterator JsonImplementType::AddValue( \ + JsonBuilder& builder, \ + bool front, \ + JsonConstIterator const& itParent, \ + std::string_view const& name, \ + DataType const& data) \ + { \ + return builder.AddValue( \ + front, itParent, name, ValueType, DataSize, DataPtr); \ + } + +#define IMPLEMENT_GetUnchecked(DataType, ValueType) \ + DataType JsonImplementType::GetUnchecked( \ + JsonValue const& value) throw() \ + { \ + return static_cast(GetUnchecked##ValueType(value)); \ + } + +#define IMPLEMENT_JsonImplementType(DataType, ValueType) \ + IMPLEMENT_AddValue(DataType, sizeof(data), &data, ValueType); \ + IMPLEMENT_GetUnchecked(DataType, ValueType); \ + \ + bool JsonImplementType::ConvertTo( \ + JsonValue const& value, DataType& result) throw() \ + { \ + return ConvertTo##ValueType(value, result); \ + } + +// JsonBool + +bool JsonImplementType::GetUnchecked(JsonValue const& value) throw() +{ + assert(value.Type() == JsonBool); + bool result; + unsigned cb; + void const* pb = value.Data(&cb); + switch (cb) + { + case 1: + result = *static_cast(pb) != 0; + break; + case 4: + result = *static_cast(pb) != 0; + break; + default: + result = 0; + assert(!"Invalid size for JsonBool"); + break; + } + return result; +} + +bool JsonImplementType::ConvertTo(JsonValue const& value, bool& result) throw() +{ + bool success; + switch (value.Type()) + { + case JsonBool: + result = GetUnchecked(value); + success = true; + break; + default: + result = false; + success = false; + break; + } + return success; +} + +IMPLEMENT_AddValue(bool, sizeof(data), &data, JsonBool); + +// JsonUInt + +bool JsonImplementType::ConvertTo( + JsonValue const& value, + unsigned long long& result) throw() +{ + static double const UnsignedHuge = 18446744073709551616.0; + + bool success; + switch (value.Type()) + { + case JsonUInt: + result = JsonImplementType::GetUnchecked(value); + success = true; + goto Done; + + case JsonInt: + result = static_cast( + JsonImplementType::GetUnchecked(value)); + if (result < 0x8000000000000000) + { + success = true; + goto Done; + } + break; + + case JsonFloat: + { + auto f = JsonImplementType::GetUnchecked(value); + if (0.0 <= f && f < UnsignedHuge) + { + result = static_cast(f); + success = true; + goto Done; + } + break; + } + + default: + break; + } + + result = 0; + success = false; + +Done: + + return success; +} + +static uint64_t GetUncheckedJsonUInt(JsonValue const& value) +{ + assert(value.Type() == JsonUInt); + uint64_t result; + unsigned cb; + void const* pb = value.Data(&cb); + switch (cb) + { + case 1: + result = *static_cast(pb); + break; + case 2: + result = *static_cast(pb); + break; + case 4: + result = *static_cast(pb); + break; + case 8: + result = *static_cast(pb); + break; + default: + result = 0; + assert(!"Invalid size for JsonUInt"); + break; + } + return result; +} + +IMPLEMENT_AddValue(unsigned long long, sizeof(data), &data, JsonUInt); +IMPLEMENT_GetUnchecked(unsigned long long, JsonUInt); + +template +static bool ConvertToJsonUInt(JsonValue const& value, T& result) +{ + unsigned long long implResult; + bool success; + if (JsonImplementType::ConvertTo(value, implResult) && + implResult <= (0xffffffffffffffff >> (64 - sizeof(T) * 8))) + { + result = static_cast(implResult); + success = true; + } + else + { + result = 0; + success = false; + } + return success; +} + +IMPLEMENT_JsonImplementType(unsigned char, JsonUInt); +IMPLEMENT_JsonImplementType(unsigned short, JsonUInt); +IMPLEMENT_JsonImplementType(unsigned int, JsonUInt); +IMPLEMENT_JsonImplementType(unsigned long, JsonUInt); + +// JsonInt + +bool JsonImplementType::ConvertTo( + JsonValue const& value, + signed long long& result) throw() +{ + static double const SignedHuge = 9223372036854775808.0; + + bool success; + switch (value.Type()) + { + case JsonInt: + result = JsonImplementType::GetUnchecked(value); + success = true; + goto Done; + + case JsonUInt: + result = static_cast( + JsonImplementType::GetUnchecked(value)); + if (result >= 0) + { + success = true; + goto Done; + } + break; + + case JsonFloat: + { + auto f = JsonImplementType::GetUnchecked(value); + if (-SignedHuge <= f && f < SignedHuge) + { + result = static_cast(f); + success = true; + goto Done; + } + break; + } + + default: + break; + } + + result = 0; + success = false; + +Done: + + return success; +} + +static int64_t GetUncheckedJsonInt(JsonValue const& value) +{ + assert(value.Type() == JsonInt); + int64_t result; + unsigned cb; + void const* pb = value.Data(&cb); + switch (cb) + { + case 1: + result = *static_cast(pb); + break; + case 2: + result = *static_cast(pb); + break; + case 4: + result = *static_cast(pb); + break; + case 8: + result = *static_cast(pb); + break; + default: + result = 0; + assert(!"Invalid size for JsonInt"); + break; + } + return result; +} + +IMPLEMENT_AddValue(signed long long, sizeof(data), &data, JsonInt); +IMPLEMENT_GetUnchecked(signed long long, JsonInt); + +template +static bool ConvertToJsonInt(JsonValue const& value, T& result) +{ + signed long long implResult; + bool success; + + if (JsonImplementType::ConvertTo(value, implResult) && + (sizeof(T) == sizeof(signed long long) || + (implResult < (1ll << (sizeof(T) * 8 - 1)) && + implResult >= -(1ll << (sizeof(T) * 8 - 1))))) + { + result = static_cast(implResult); + success = true; + } + else + { + result = 0; + success = false; + } + return success; +} + +IMPLEMENT_JsonImplementType(signed char, JsonInt); +IMPLEMENT_JsonImplementType(signed short, JsonInt); +IMPLEMENT_JsonImplementType(signed int, JsonInt); +IMPLEMENT_JsonImplementType(signed long, JsonInt); + +// JsonFloat + +double JsonImplementType::GetUnchecked(JsonValue const& value) throw() +{ + assert(value.Type() == JsonFloat); + double result; + unsigned cb; + void const* pb = value.Data(&cb); + switch (cb) + { + case 4: + result = *static_cast(pb); + break; + case 8: + result = *static_cast(pb); + break; + default: + result = 0; + assert(!"Invalid size for JsonFloat"); + break; + } + return result; +} + +bool JsonImplementType::ConvertTo( + JsonValue const& value, + double& result) throw() +{ + bool success; + + switch (value.Type()) + { + case JsonUInt: + result = static_cast( + JsonImplementType::GetUnchecked(value)); + success = true; + break; + + case JsonInt: + result = static_cast( + JsonImplementType::GetUnchecked(value)); + success = true; + break; + + case JsonFloat: + result = GetUnchecked(value); + success = true; + break; + + default: + result = 0.0; + success = false; + break; + } + + return success; +} + +IMPLEMENT_AddValue(double, sizeof(data), &data, JsonFloat); + +#define GetUncheckedJsonFloat(value) \ + JsonImplementType::GetUnchecked(value) + +template +static bool ConvertToJsonFloat(JsonValue const& value, T& result) +{ + double implResult; + bool success = JsonImplementType::ConvertTo(value, implResult); + result = static_cast(implResult); + return success; +} + +IMPLEMENT_JsonImplementType(float, JsonFloat); + +// JsonUtf8 + +JsonIterator JsonImplementType::AddValue( + JsonBuilder& builder, + bool front, + JsonConstIterator const& itParent, + std::string_view const& name, + char const* psz) +{ + return builder.AddValue( + front, itParent, name, JsonUtf8, static_cast(strlen(psz)), psz); +} + +IMPLEMENT_AddValue(char, sizeof(data), &data, JsonUtf8); +IMPLEMENT_AddValue(std::string, data.size(), data.data(), JsonUtf8); + +std::string_view +JsonImplementType::GetUnchecked(JsonValue const& value) throw() +{ + assert(value.Type() == JsonUtf8); + unsigned cb; + void const* pb = value.Data(&cb); + return std::string_view(static_cast(pb), cb); +} + +bool JsonImplementType::ConvertTo( + JsonValue const& value, + std::string_view& result) throw() +{ + bool success; + + if (value.Type() == JsonUtf8) + { + result = GetUnchecked(value); + success = true; + } + else + { + result = std::string_view(); + success = false; + } + + return success; +} + +JsonIterator JsonImplementType::AddValue( + JsonBuilder& builder, + bool front, + JsonConstIterator const& itParent, + std::string_view const& name, + std::string_view const& data) +{ + return builder.AddValue( + front, + itParent, + name, + JsonUtf8, + static_cast(data.size()), + data.data()); +} + +// JsonTime + +JsonIterator JsonImplementType::AddValue( + JsonBuilder& builder, + bool front, + JsonConstIterator const& itParent, + std::string_view const& name, + std::chrono::system_clock::time_point const& data) +{ + std::chrono::nanoseconds nanosSinceEpoch = + std::chrono::duration_cast( + data.time_since_epoch()); + + int64_t nanosInt64 = nanosSinceEpoch.count(); + return builder.AddValue( + front, itParent, name, JsonTime, sizeof(nanosInt64), &nanosInt64); +} + +bool JsonImplementType::ConvertTo( + JsonValue const& jsonValue, + std::chrono::system_clock::time_point& value) throw() +{ + bool success; + + if (jsonValue.Type() == JsonTime) + { + value = jsonValue.GetUnchecked(); + success = true; + } + else + { + value = std::chrono::system_clock::time_point{}; + success = false; + } + + return success; +} + +std::chrono::system_clock::time_point +JsonImplementType::GetUnchecked( + JsonValue const& jsonValue) throw() +{ + assert(jsonValue.Type() == JsonTime); + assert(jsonValue.DataSize() == 8); + if (jsonValue.DataSize() == 8) + { + int64_t nanosSinceEpoch = *static_cast(jsonValue.Data()); + return std::chrono::system_clock::time_point{ std::chrono::nanoseconds{ + nanosSinceEpoch } }; + } + else + { + return {}; + } +} + +// JsonUuid + +static const UuidStruct k_emptyUuid = {}; + +IMPLEMENT_AddValue(UuidStruct, sizeof(UuidStruct), &data, JsonUuid); + +bool JsonImplementType::ConvertTo( + JsonValue const& jsonValue, + UuidStruct& value) throw() +{ + bool success; + + if (jsonValue.Type() == JsonUuid) + { + assert(jsonValue.DataSize() == 16); + value = *static_cast(jsonValue.Data()); + success = true; + } + else + { + value = k_emptyUuid; + success = false; + } + + return success; +} + +UuidStruct +JsonImplementType::GetUnchecked(JsonValue const& jsonValue) throw() +{ + assert(jsonValue.Type() == JsonUuid); + assert(jsonValue.DataSize() == 16); + + return jsonValue.DataSize() == 16 ? + *static_cast(jsonValue.Data()) : + k_emptyUuid; +} + +} diff --git a/src/JsonRenderer.cpp b/src/JsonRenderer.cpp new file mode 100644 index 0000000..de0d6be --- /dev/null +++ b/src/JsonRenderer.cpp @@ -0,0 +1,485 @@ +#include "jsonbuilder/JsonRenderer.h" + +#include +#include + +#include + + +#define WriteChar(ch) m_renderBuffer.push_back(ch) +#define WriteChars(pch, cch) m_renderBuffer.append(pch, cch) + +namespace jsonbuilder +{ +/* +Efficiently multiply two 32-bit unsigned integers to get a 64-bit result. +(The current VC compiler does not optimize this -- if we don't use an +intrinsic, it makes a call to _aullmul.) +*/ +#if defined(_M_ARM) || defined(_M_ARM64) +# define UMul64(a, b) _arm_umull(a, b) +#elif defined(_M_IX86) || defined(_M_X64) +# define UMul64(a, b) __emulu(a, b) +#else +static long long unsigned UMul64(unsigned a, unsigned b) +{ + return static_cast(a) * b; +} +#endif + +/* + Given a number from 0..15, returns the corresponding uppercase hex digit, + i.e. '0'..'F'. Note: it is an error to pass a value larger than 15. +*/ +static inline constexpr char u4_to_hex_upper(char unsigned _Val4) +{ + return ("0123456789ABCDEF")[_Val4]; +} + +/* +Multiply two 64-bit unsigned integers to get a 128-bit result. Return the high +64 bits of the answer. +*/ +__attribute__((unused)) static long long unsigned +UMul128Hi(long long unsigned a, long long unsigned b) +{ +#if defined(_M_X64) || defined(_M_ARM64) + long long unsigned const high = __umulh(a, b); +#else + long long unsigned const mid = + UMul64(static_cast(a), static_cast(b >> 32)) + + (UMul64(static_cast(a), static_cast(b)) >> 32); + long long unsigned const high = + UMul64(static_cast(a >> 32), static_cast(b >> 32)) + + (mid >> 32) + + ((UMul64(static_cast(a >> 32), static_cast(b)) + + static_cast(mid)) >> + 32); +#endif + return high; +} + +/* +Formats a uint32 with leading 0s. Always writes cch characters. +*/ +static void FormatUint(unsigned n, char* pch, unsigned cch) +{ + do + { + --cch; + pch[cch] = '0' + (n % 10); + n = n / 10; + } while (cch != 0); +} + +template +static unsigned JsonRenderXInt(N const& n, char* pBuffer) +{ + // TODO: Why can't we use std::to_wchars when we're using c++17? + std::string result = std::to_string(n); + std::size_t const cch = + std::min(result.size(), static_cast(CB - 1)); + strncpy(pBuffer, result.c_str(), cch); + pBuffer[cch] = 0; + + pBuffer[cch] = 0; + return static_cast(cch); +} + +unsigned JsonRenderUInt(long long unsigned n, char* pBuffer) throw() +{ + return JsonRenderXInt<21>(n, pBuffer); +} + +unsigned JsonRenderInt(long long signed n, char* pBuffer) throw() +{ + return JsonRenderXInt<21>(n, pBuffer); +} + +unsigned JsonRenderFloat(double n, char* pBuffer) throw() +{ + unsigned cch; + + if (isfinite(n)) + { + cch = static_cast(snprintf(pBuffer, 31, "%.17g", n)); + if (cch > 31) + { + cch = 31; + } + pBuffer[cch] = 0; + } + else + { + cch = JsonRenderNull(pBuffer); + } + + return cch; +} + +unsigned JsonRenderBool(bool b, char* pBuffer) throw() +{ + unsigned cch; + if (b) + { + strcpy(pBuffer, "true"); + cch = 4; + } + else + { + strcpy(pBuffer, "false"); + cch = 5; + } + return cch; +} + +unsigned JsonRenderNull(char* pBuffer) throw() +{ + strcpy(pBuffer, "null"); + return 4; +} + +unsigned JsonRenderTime( + std::chrono::system_clock::time_point const& timePoint, + char* pBuffer) throw() +{ + time_t printableTime = std::chrono::system_clock::to_time_t(timePoint); + tm timeStruct = {}; + gmtime_r(&printableTime, &timeStruct); + + auto subsecondDuration = + timePoint.time_since_epoch() % std::chrono::seconds{ 1 }; + auto subsecondNanos = + std::chrono::duration_cast(subsecondDuration); + + FormatUint(static_cast(timeStruct.tm_year + 1900), pBuffer + 0, 4); + pBuffer[4] = '-'; + FormatUint(static_cast(timeStruct.tm_mon + 1), pBuffer + 5, 2); + pBuffer[7] = '-'; + FormatUint(static_cast(timeStruct.tm_mday), pBuffer + 8, 2); + pBuffer[10] = 'T'; + FormatUint(static_cast(timeStruct.tm_hour), pBuffer + 11, 2); + pBuffer[13] = ':'; + FormatUint(static_cast(timeStruct.tm_min), pBuffer + 14, 2); + pBuffer[16] = ':'; + FormatUint(static_cast(timeStruct.tm_sec), pBuffer + 17, 2); + pBuffer[19] = '.'; + FormatUint( + static_cast(subsecondNanos.count() / 100), pBuffer + 20, 7); + pBuffer[27] = 'Z'; + pBuffer[28] = 0; + return 28; +} + +unsigned JsonRenderUuid(uuid_t const& g, char* pBuffer) throw() +{ + uuid_unparse_upper(g, pBuffer); + return 36; +} + +unsigned JsonRenderUuidWithBraces(uuid_t const& g, char* pBuffer) throw() +{ + pBuffer[0] = '{'; + JsonRenderUuid(g, pBuffer + 1); + pBuffer[37] = '}'; + pBuffer[38] = 0; + return 38; +} + +JsonRenderer::JsonRenderer( + bool pretty, + std::string_view const& newLine, + unsigned indentSpaces) throw() + : m_newLine(newLine), m_indentSpaces(indentSpaces), m_pretty(pretty) +{ + return; +} + +void JsonRenderer::Reserve(size_type cb) +{ + m_renderBuffer.reserve(cb); +} + +JsonRenderer::size_type JsonRenderer::Size() const throw() +{ + return m_renderBuffer.size(); +} + +JsonRenderer::size_type JsonRenderer::Capacity() const throw() +{ + return m_renderBuffer.capacity(); +} + +bool JsonRenderer::Pretty() const throw() +{ + return m_pretty; +} + +void JsonRenderer::Pretty(bool value) throw() +{ + m_pretty = value; +} + +std::string_view const& JsonRenderer::NewLine() const throw() +{ + return m_newLine; +} + +void JsonRenderer::NewLine(std::string_view const& value) throw() +{ + m_newLine = value; +} + +unsigned JsonRenderer::IndentSpaces() const throw() +{ + return m_indentSpaces; +} + +void JsonRenderer::IndentSpaces(unsigned value) throw() +{ + m_indentSpaces = value; +} + +std::string_view JsonRenderer::Render(JsonBuilder const& builder) +{ + auto itRoot = builder.end(); + m_renderBuffer.clear(); + m_indent = 0; + RenderStructure(itRoot, true); + WriteChar('\0'); + return std::string_view(m_renderBuffer.data(), m_renderBuffer.size() - 1); +} + +std::string_view JsonRenderer::Render(JsonBuilder::const_iterator const& it) +{ + m_renderBuffer.clear(); + m_indent = 0; + if (it.IsRoot()) + { + RenderStructure(it, true); + } + else + { + RenderValue(it); + } + WriteChar('\0'); + return std::string_view(m_renderBuffer.data(), m_renderBuffer.size() - 1); +} + +void JsonRenderer::RenderCustom(RenderBuffer&, iterator const& it) +{ + auto const cchMax = 32u; + auto pch = m_renderBuffer.GetAppendPointer(cchMax); + unsigned cch = + static_cast(snprintf(pch, cchMax, "\"Custom#%u\"", it->Type())); + pch += cch > cchMax ? cchMax : cch; + m_renderBuffer.SetEndPointer(pch); +} + +void JsonRenderer::RenderValue(iterator const& it) +{ + assert(!it.IsRoot()); + switch (it->Type()) + { + case JsonObject: + RenderStructure(it, true); + break; + case JsonArray: + RenderStructure(it, false); + break; + case JsonNull: + WriteChars("null", 4); + break; + case JsonBool: + if (it->GetUnchecked()) + { + WriteChars("true", 4); + } + else + { + WriteChars("false", 5); + } + break; + case JsonUtf8: + RenderString(it->GetUnchecked()); + break; + case JsonFloat: + RenderFloat(it->GetUnchecked()); + break; + case JsonInt: + RenderInt(it->GetUnchecked()); + break; + case JsonUInt: + RenderUInt(it->GetUnchecked()); + break; + case JsonTime: + RenderTime(it->GetUnchecked()); + break; + case JsonUuid: + RenderUuid(it->GetUnchecked().Data); + break; + default: + RenderCustom(m_renderBuffer, it); + break; + } +} + +void JsonRenderer::RenderStructure(iterator const& itParent, bool showNames) +{ + WriteChar(showNames ? '{' : '['); + + auto it = itParent.begin(); + auto itEnd = itParent.end(); + if (it != itEnd) + { + m_indent += m_indentSpaces; + + for (;;) + { + if (m_pretty) + { + RenderNewline(); + } + + if (showNames) + { + RenderString(it->Name()); + WriteChar(':'); + + if (m_pretty) + { + WriteChar(' '); + } + } + + RenderValue(it); + + ++it; + if (it == itEnd) + { + break; + } + + WriteChar(','); + } + + m_indent -= m_indentSpaces; + + if (m_pretty) + { + RenderNewline(); + } + } + + WriteChar(showNames ? '}' : ']'); +} + +void JsonRenderer::RenderFloat(double const& value) +{ + auto pch = m_renderBuffer.GetAppendPointer(32); + pch += JsonRenderFloat(value, pch); + m_renderBuffer.SetEndPointer(pch); +} + +void JsonRenderer::RenderInt(long long signed const& value) +{ + auto const cchMax = 20u; + auto pch = m_renderBuffer.GetAppendPointer(cchMax); + + // TODO: Why can't we use std::to_wchars when we're using c++17? + unsigned cch = static_cast(snprintf(pch, cchMax, "%lld", value)); + pch += cch > cchMax ? cchMax : cch; + m_renderBuffer.SetEndPointer(pch); +} + +void JsonRenderer::RenderUInt(long long unsigned const& value) +{ + auto const cchMax = 20u; + auto pch = m_renderBuffer.GetAppendPointer(cchMax); + + // TODO: Why can't we use std::to_wchars when we're using c++17? + unsigned cch = static_cast(snprintf(pch, cchMax, "%llu", value)); + pch += cch > cchMax ? cchMax : cch; + m_renderBuffer.SetEndPointer(pch); +} + +void JsonRenderer::RenderTime(std::chrono::system_clock::time_point const& value) +{ + auto pch = m_renderBuffer.GetAppendPointer(32); + *pch++ = '"'; + pch += JsonRenderTime(value, pch); + *pch++ = '"'; + m_renderBuffer.SetEndPointer(pch); +} + +void JsonRenderer::RenderUuid(uuid_t const& value) +{ + auto pch = m_renderBuffer.GetAppendPointer(38); + *pch++ = '"'; + pch += JsonRenderUuid(value, pch); + *pch++ = '"'; + m_renderBuffer.SetEndPointer(pch); +} + +void JsonRenderer::RenderString(std::string_view const& value) +{ + WriteChar('"'); + for (auto ch : value) + { + if (ch >= 0 && ch < 0x20) + { + // Control character - must be escaped. + switch (ch) + { + case 8: + WriteChar('\\'); + WriteChar('b'); + break; + case 9: + WriteChar('\\'); + WriteChar('t'); + break; + case 10: + WriteChar('\\'); + WriteChar('n'); + break; + case 12: + WriteChar('\\'); + WriteChar('f'); + break; + case 13: + WriteChar('\\'); + WriteChar('r'); + break; + default: + auto p = m_renderBuffer.GetAppendPointer(6); + *p++ = '\\'; + *p++ = 'u'; + *p++ = '0'; + *p++ = '0'; + *p++ = u4_to_hex_upper((ch >> 4) & 0xf); + *p++ = u4_to_hex_upper(ch & 0xf); + m_renderBuffer.SetEndPointer(p); + break; + } + } + else if (ch == '"' || ch == '\\') + { + // ASCII character - pass through (escape quote and backslash) + WriteChar('\\'); + WriteChar(ch); + } + else + { + WriteChar(ch); + } + } + WriteChar('"'); +} + +void JsonRenderer::RenderNewline() +{ + WriteChars(m_newLine.data(), static_cast(m_newLine.size())); + m_renderBuffer.append(m_indent, ' '); +} +} \ No newline at end of file diff --git a/src/PodVector.cpp b/src/PodVector.cpp new file mode 100644 index 0000000..788a7d6 --- /dev/null +++ b/src/PodVector.cpp @@ -0,0 +1,124 @@ +#include + +#include "jsonbuilder/JsonBuilder.h" + + +namespace jsonbuilder +{ +// +// TODO: +// Copied from OS code +// with a bug fix, since (1 << 32) is undefined in the c standard and evaluates +// to 1 on some hardware. Try to find a better source or update PodVector.cpp to +// not rely on this. +// +static inline unsigned char +BitScanReverse(unsigned long* Index, unsigned long Mask) +{ + if (Mask == 0 || Index == 0) + return 0; + + unsigned int ii = 0; + for (ii = ((sizeof(Mask) * 8) - 1); ii >= 0; --ii) + { + unsigned long tempMask = 1 << ii; + if ((Mask & tempMask) != 0) + { + *Index = ii; + break; + } + } + return (ii >= 0 ? (unsigned char) 1 : (unsigned char) 0); +} + +namespace JsonInternal +{ +void PodVectorBase::CheckOffset(size_type index, size_type currentSize) throw() +{ + (void) index; // Unreferenced parameter + (void) currentSize; // Unreferenced parameter + assert(index < currentSize); +} + +void PodVectorBase::CheckRange(void const* p1, void const* p2, void const* p3) throw() +{ + (void) p1; // Unreferenced parameter + (void) p2; // Unreferenced parameter + (void) p3; // Unreferenced parameter + assert(p1 <= p2 && p2 <= p3); +} + +PodVectorBase::size_type PodVectorBase::CheckedAdd(size_type a, size_type b) +{ + size_type c = a + b; + if (c < a) + { + throw std::length_error("JsonVector - exceeded maximum capacity"); + } + return c; +} + +void PodVectorBase::InitData(void* pDest, void const* pSource, size_t cb) throw() +{ + memcpy(pDest, pSource, cb); +} + +PodVectorBase::size_type +PodVectorBase::GetNewCapacity(size_type minCapacity, size_type maxCapacity) +{ + size_type cap; + if (minCapacity <= 15) + { + cap = 15; + } + else + { + long unsigned index; + BitScanReverse(&index, minCapacity); + cap = static_cast((2 << index) - 1); + } + + if (maxCapacity < cap) + { + if (maxCapacity < minCapacity) + { + throw std::length_error("JsonVector - exceeded maximum capacity"); + } + + cap = maxCapacity; + } + + assert(15 <= cap); + assert(minCapacity <= cap); + assert(cap <= maxCapacity); + return cap; +} + +void* PodVectorBase::Reallocate(void* pb, size_t cb, bool zeroInitializeMemory) +{ + void* const pbNew = pb ? realloc(pb, cb) : malloc(cb); + + if (pbNew == nullptr) + { + throw std::bad_alloc(); + } + + if (zeroInitializeMemory) + { + memset(pbNew, 0, cb); + } + + return pbNew; +} + +void PodVectorBase::Deallocate(void* pb) throw() +{ + if (pb) + { + free(pb); + } +} +} +// namespace JsonInternal + +} \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..91c8a5d --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.12) + +# Add source to this project's executable. +add_executable(jsonbuilderTest CatchMain.cpp TestBuilder.cpp TestRenderer.cpp) +target_link_libraries(jsonbuilderTest PRIVATE jsonbuilder Catch2::Catch2) + +set_property(TARGET jsonbuilderTest PROPERTY CXX_STANDARD 17) + +include(CTest) +include(Catch) +catch_discover_tests(jsonbuilderTest) + +install(TARGETS jsonbuilderTest DESTINATION bin) diff --git a/test/CatchMain.cpp b/test/CatchMain.cpp new file mode 100644 index 0000000..4ed06df --- /dev/null +++ b/test/CatchMain.cpp @@ -0,0 +1,2 @@ +#define CATCH_CONFIG_MAIN +#include diff --git a/test/TestBuilder.cpp b/test/TestBuilder.cpp new file mode 100644 index 0000000..d421cc5 --- /dev/null +++ b/test/TestBuilder.cpp @@ -0,0 +1,630 @@ +#include +#include + +using namespace jsonbuilder; + +TEST_CASE("JsonBuilder buffer reserve", "[builder]") +{ + constexpr auto c_maxSize = JsonBuilder::buffer_max_size(); + + JsonBuilder b; + + SECTION("Buffer begins empty") + { + REQUIRE(b.buffer_size() == 0); + REQUIRE(b.buffer_capacity() == 0); + } + + SECTION("Buffer reserving 0 remains empty") + { + b.buffer_reserve(0); + REQUIRE(b.buffer_size() == 0); + REQUIRE(b.buffer_capacity() == 0); + } + + SECTION("Buffer reserving 1 keeps size 0 and makes a capacity of at least " + "the size of one element") + { + b.buffer_reserve(1); + REQUIRE(b.buffer_size() == 0); + REQUIRE(b.buffer_capacity() >= 4); + } + + SECTION("Buffer reserving 2 keeps size 0 and makes a capacity of at least " + "the size of one element") + { + b.buffer_reserve(2); + REQUIRE(b.buffer_size() == 0); + REQUIRE(b.buffer_capacity() >= 4); + } + + SECTION("Buffer reserving 5 keeps size 0 and makes a capacity of at least " + "the size of two elements") + { + b.buffer_reserve(5); + REQUIRE(b.buffer_size() == 0); + REQUIRE(b.buffer_capacity() >= 8); + } + + SECTION("Buffer reserving max succeeds or throws std::bad_alloc") + { + auto reserveMax = [&]() { + try + { + b.buffer_reserve(c_maxSize); + } + catch (const std::bad_alloc&) + {} + }; + + REQUIRE_NOTHROW(reserveMax()); + } + + SECTION("Buffer reserving one more than max throws std::length_error") + { + REQUIRE_THROWS_AS(b.buffer_reserve(c_maxSize + 1), std::length_error); + } + + SECTION("Buffer reserving maximum of size_type throws std::length_error") + { + auto maxSizeType = ~JsonBuilder::size_type(0); + REQUIRE_THROWS_AS(b.buffer_reserve(maxSizeType), std::length_error); + } +} + +template +static void TestInputOutputScalar() +{ + using InputLimits = std::numeric_limits; + using OutputLimits = std::numeric_limits; + + JsonBuilder b; + + b.push_back(b.end(), "", InputLimits::lowest()); + b.push_back(b.end(), "", InputLimits::min()); + b.push_back(b.end(), "", InputLimits::max()); + b.push_back(b.end(), "", OutputLimits::lowest()); + b.push_back(b.end(), "", OutputLimits::min()); + b.push_back(b.end(), "", OutputLimits::max()); + + REQUIRE_NOTHROW(b.ValidateData()); + + InputType i; + + auto it = b.begin(); + REQUIRE(it->GetUnchecked() == InputLimits::lowest()); + REQUIRE(it->ConvertTo(i)); + REQUIRE(i == InputLimits::lowest()); + + ++it; + REQUIRE(it->GetUnchecked() == InputLimits::min()); + REQUIRE(it->ConvertTo(i)); + REQUIRE(i == InputLimits::min()); + + ++it; + REQUIRE(it->GetUnchecked() == InputLimits::max()); + REQUIRE(it->ConvertTo(i)); + REQUIRE(i == InputLimits::max()); + + ++it; + REQUIRE( + it->GetUnchecked() == + static_cast(OutputLimits::lowest())); + if (it->ConvertTo(i)) + { + REQUIRE(i == static_cast(OutputLimits::lowest())); + } + else + { + REQUIRE(i == 0); + REQUIRE(it->GetUnchecked() != OutputLimits::lowest()); + } + + ++it; + REQUIRE( + it->GetUnchecked() == + static_cast(OutputLimits::min())); + if (it->ConvertTo(i)) + { + REQUIRE(i == static_cast(OutputLimits::min())); + } + else + { + REQUIRE(i == 0); + REQUIRE(it->GetUnchecked() != OutputLimits::min()); + } + + ++it; + REQUIRE( + it->GetUnchecked() == + static_cast(OutputLimits::max())); + if (it->ConvertTo(i)) + { + REQUIRE(i == static_cast(OutputLimits::max())); + } + else + { + REQUIRE(i == 0); + REQUIRE(it->GetUnchecked() != OutputLimits::max()); + } +} + +TEST_CASE("JsonBuilder numeric limits", "[builder]") +{ + SECTION("signed char") { TestInputOutputScalar(); } + SECTION("signed short") { TestInputOutputScalar(); } + SECTION("signed int") { TestInputOutputScalar(); } + SECTION("signed long") { TestInputOutputScalar(); } + SECTION("signed long long") + { + TestInputOutputScalar(); + } + + SECTION("unsigned char") + { + TestInputOutputScalar(); + } + SECTION("unsigned short") + { + TestInputOutputScalar(); + } + SECTION("unsigned int") { TestInputOutputScalar(); } + SECTION("unsigned long") + { + TestInputOutputScalar(); + } + SECTION("unsigned long long") + { + TestInputOutputScalar(); + } + + SECTION("float") { TestInputOutputScalar(); } + SECTION("double") { TestInputOutputScalar(); } +} + +TEST_CASE("JsonBuilder string push_back") +{ + JsonBuilder b; + + SECTION("push_back std::string_view") + { + auto itr = b.push_back(b.end(), "", std::string_view{ "ABCDE" }); + REQUIRE(itr->GetUnchecked() == "ABCDE"); + } + + SECTION("push_back std::string") + { + auto itr = b.push_back(b.end(), "", std::string{ "ABCDE" }); + REQUIRE(itr->GetUnchecked() == "ABCDE"); + } + + SECTION("push_back char") + { + auto itr = b.push_back(b.end(), "", ' '); + REQUIRE(itr->GetUnchecked() == " "); + } + + SECTION("push_back char*") + { + auto itr = b.push_back(b.end(), "", const_cast("ABC")); + REQUIRE(itr->GetUnchecked() == "ABC"); + } + + SECTION("push_back const char*") + { + auto itr = b.push_back(b.end(), "", static_cast("DEF")); + REQUIRE(itr->GetUnchecked() == "DEF"); + } + + SECTION("push_back const char[]") + { + auto itr = b.push_back(b.end(), "", "HIJ"); + REQUIRE(itr->GetUnchecked() == "HIJ"); + } +} + +TEST_CASE("JsonBuilder chrono push_back", "[builder]") +{ + auto now = std::chrono::system_clock::now(); + + JsonBuilder b; + auto itr = b.push_back(b.end(), "CurrentTime", now); + auto retrieved = itr->GetUnchecked(); + REQUIRE(retrieved == now); +} + +TEST_CASE("JsonBuilder uuid push_back", "[builder]") +{ + UuidStruct uuid; + uuid_generate(uuid.Data); + + JsonBuilder b; + auto itr = b.push_back(b.end(), "Uuid", uuid); + + auto retrieved = itr->GetUnchecked(); + REQUIRE(uuid_compare(retrieved.Data, uuid.Data) == 0); +} + +TEST_CASE("JsonBuilder find", "[builder]") +{ + JsonBuilder b; + + // Empty object + REQUIRE(b.find("a1") == b.end()); + REQUIRE(b.find("a1", "a2") == b.end()); + + // Single object (a1) + auto itA1 = b.push_back(b.end(), "a1", JsonObject); + REQUIRE(b.find("a1") == itA1); + REQUIRE(b.find(b.end(), "a1") == itA1); + REQUIRE(b.find("b1") == b.end()); + REQUIRE(b.find(b.end(), "b1") == b.end()); + REQUIRE(b.find("a1", "a2") == b.end()); + + // Second object b2, sibling of a1 + auto itB1 = b.push_back(b.end(), "b1", JsonObject); + REQUIRE(b.find("a1") == itA1); + REQUIRE(b.find("a1", "a2") == b.end()); + REQUIRE(b.find("b1") == itB1); + REQUIRE(b.find("c1") == b.end()); + + // First child of a1, a2 + auto itA1A2 = b.push_back(itA1, "a2", JsonObject); + REQUIRE(b.find("a1") == itA1); + REQUIRE(b.find("a1", "a2") == itA1A2); + REQUIRE(b.find(b.end(), "a1", "a2") == itA1A2); + REQUIRE(b.find("a1", "a2", "a3") == b.end()); + REQUIRE(b.find("b1") == itB1); + REQUIRE(b.find("c1") == b.end()); + + // First child of a2, a3 + auto itA1A2A3 = b.push_back(itA1A2, "a3", 0); + REQUIRE(b.find("a1", "a2", "a3") == itA1A2A3); + REQUIRE(b.find(itA1, "a2") == itA1A2); + REQUIRE(b.find(itB1, "a2") == b.end()); + + REQUIRE_NOTHROW(b.ValidateData()); +} + +TEST_CASE("JsonBuilder constructors", "[builder]") +{ + JsonBuilder b; + b.push_back(b.end(), "aname", "ava"); + b.push_back(b.end(), "bname", "bva"); + REQUIRE_NOTHROW(b.ValidateData()); + + SECTION("Copy constructor") + { + JsonBuilder copy{ b }; + REQUIRE_NOTHROW(copy.ValidateData()); + + auto it = copy.begin(); + REQUIRE(it->Name() == "aname"); + REQUIRE(it->GetUnchecked() == "ava"); + + ++it; + REQUIRE(it->Name() == "bname"); + REQUIRE(it->GetUnchecked() == "bva"); + } + + SECTION("Move constructor") + { + JsonBuilder move{ std::move(b) }; + REQUIRE_NOTHROW(move.ValidateData()); + + auto it = move.begin(); + REQUIRE(it->Name() == "aname"); + REQUIRE(it->GetUnchecked() == "ava"); + + ++it; + REQUIRE(it->Name() == "bname"); + REQUIRE(it->GetUnchecked() == "bva"); + } +} + +TEST_CASE("JsonBuilder erase", "[builder]") +{ + JsonBuilder b; + b.push_back(b.end(), "aname", "ava"); + b.push_back(b.end(), "bname", "bva"); + REQUIRE_NOTHROW(b.ValidateData()); + + SECTION("erase a single child element") + { + auto itr = b.erase(b.begin()); + REQUIRE_NOTHROW(b.ValidateData()); + REQUIRE(itr == b.begin()); + REQUIRE(b.count(b.end()) == 1); + } + + SECTION("erase all children") + { + auto itr = b.erase(b.begin(), b.end()); + REQUIRE_NOTHROW(b.ValidateData()); + REQUIRE(itr == b.end()); + REQUIRE(b.begin() == b.end()); + REQUIRE(b.count(itr) == 0); + } +} + +TEST_CASE("JsonBuilder conversions", "[builder]") +{ + int64_t ival; + uint64_t uval; + double fval; + std::string_view sval; + bool bval; + std::chrono::system_clock::time_point tval; + UuidStruct uuidval; + + JsonBuilder b; + + SECTION("JsonNull") + { + auto itr = b.push_back(b.end(), "FirstItem", JsonNull); + + REQUIRE(itr->IsNull()); + REQUIRE(!itr->ConvertTo(bval)); + REQUIRE(!itr->ConvertTo(fval)); + REQUIRE(!itr->ConvertTo(ival)); + REQUIRE(!itr->ConvertTo(uval)); + REQUIRE(!itr->ConvertTo(sval)); + REQUIRE(!itr->ConvertTo(tval)); + REQUIRE(!itr->ConvertTo(uuidval)); + } + + SECTION("false") + { + auto itr = b.push_back(b.end(), "", false); + + REQUIRE(itr->GetUnchecked() == false); + REQUIRE((itr->ConvertTo(bval) && !bval)); + REQUIRE(!itr->ConvertTo(fval)); + REQUIRE(!itr->ConvertTo(ival)); + REQUIRE(!itr->ConvertTo(uval)); + REQUIRE(!itr->ConvertTo(sval)); + REQUIRE(!itr->ConvertTo(tval)); + REQUIRE(!itr->ConvertTo(uuidval)); + } + + SECTION("true") + { + auto itr = b.push_back(b.end(), "", true); + + REQUIRE(itr->GetUnchecked() == true); + REQUIRE((itr->ConvertTo(bval) && bval)); + REQUIRE(!itr->ConvertTo(fval)); + REQUIRE(!itr->ConvertTo(ival)); + REQUIRE(!itr->ConvertTo(uval)); + REQUIRE(!itr->ConvertTo(sval)); + REQUIRE(!itr->ConvertTo(tval)); + REQUIRE(!itr->ConvertTo(uuidval)); + } + + SECTION("int64_t") + { + auto itr = b.push_back(b.end(), "", 123); + + REQUIRE(itr->GetUnchecked() == 123); + REQUIRE(!itr->ConvertTo(bval)); + REQUIRE((itr->ConvertTo(fval) && fval == 123)); + REQUIRE((itr->ConvertTo(ival) && ival == 123)); + REQUIRE((itr->ConvertTo(uval) && uval == 123)); + REQUIRE(!itr->ConvertTo(sval)); + REQUIRE(!itr->ConvertTo(tval)); + REQUIRE(!itr->ConvertTo(uuidval)); + } + + SECTION("uint64_t") + { + auto itr = b.push_back(b.end(), "", 123u); + + REQUIRE(itr->GetUnchecked() == 123u); + REQUIRE(!itr->ConvertTo(bval)); + REQUIRE((itr->ConvertTo(fval) && fval == 123)); + REQUIRE((itr->ConvertTo(ival) && ival == 123)); + REQUIRE((itr->ConvertTo(uval) && uval == 123)); + REQUIRE(!itr->ConvertTo(sval)); + REQUIRE(!itr->ConvertTo(tval)); + REQUIRE(!itr->ConvertTo(uuidval)); + } + + SECTION("double") + { + auto itr = b.push_back(b.end(), "", 123.0); + + REQUIRE(itr->GetUnchecked() == 123.0); + REQUIRE(!itr->ConvertTo(bval)); + REQUIRE((itr->ConvertTo(fval) && fval == 123)); + REQUIRE((itr->ConvertTo(ival) && ival == 123)); + REQUIRE((itr->ConvertTo(uval) && uval == 123)); + REQUIRE(!itr->ConvertTo(sval)); + REQUIRE(!itr->ConvertTo(tval)); + REQUIRE(!itr->ConvertTo(uuidval)); + } + + SECTION("string") + { + auto itr = b.push_back(b.end(), "", "ABC"); + + REQUIRE(itr->GetUnchecked() == "ABC"); + REQUIRE(!itr->ConvertTo(bval)); + REQUIRE(!itr->ConvertTo(fval)); + REQUIRE(!itr->ConvertTo(ival)); + REQUIRE(!itr->ConvertTo(uval)); + REQUIRE((itr->ConvertTo(sval) && sval == "ABC")); + REQUIRE(!itr->ConvertTo(tval)); + REQUIRE(!itr->ConvertTo(uuidval)); + } + + SECTION("less than int64_t min as double") + { + auto itr = b.push_back(b.end(), "", -9223372036854777856.0); + + REQUIRE(!itr->ConvertTo(bval)); + REQUIRE((itr->ConvertTo(fval) && fval == -9223372036854777856.0)); + REQUIRE(!itr->ConvertTo(ival)); + REQUIRE(!itr->ConvertTo(uval)); + REQUIRE(!itr->ConvertTo(sval)); + REQUIRE(!itr->ConvertTo(tval)); + REQUIRE(!itr->ConvertTo(uuidval)); + } + + SECTION("int64_t min") + { + // This value of -9223372036854775808 cannot be described as a literal + // due to negative literals being positive literals with a unary minus + // applied. + constexpr int64_t c_int64min = (-9223372036854775807ll - 1); + + auto itr = b.push_back(b.end(), "", c_int64min); + + REQUIRE(!itr->ConvertTo(bval)); + REQUIRE((itr->ConvertTo(fval) && fval == -9223372036854775808.0)); + REQUIRE((itr->ConvertTo(ival) && ival == c_int64min)); + REQUIRE(!itr->ConvertTo(uval)); + REQUIRE(!itr->ConvertTo(sval)); + REQUIRE(!itr->ConvertTo(tval)); + REQUIRE(!itr->ConvertTo(uuidval)); + } + + SECTION("-1") + { + auto itr = b.push_back(b.end(), "", -1); + + REQUIRE(!itr->ConvertTo(bval)); + REQUIRE((itr->ConvertTo(fval) && fval == -1.0)); + REQUIRE((itr->ConvertTo(ival) && ival == -1)); + REQUIRE(!itr->ConvertTo(uval)); + REQUIRE(!itr->ConvertTo(sval)); + REQUIRE(!itr->ConvertTo(tval)); + REQUIRE(!itr->ConvertTo(uuidval)); + } + + SECTION("0") + { + auto itr = b.push_back(b.end(), "", 0); + + REQUIRE(!itr->ConvertTo(bval)); + REQUIRE((itr->ConvertTo(fval) && fval == 0.0)); + REQUIRE((itr->ConvertTo(ival) && ival == 0)); + REQUIRE((itr->ConvertTo(uval) && uval == 0)); + REQUIRE(!itr->ConvertTo(sval)); + REQUIRE(!itr->ConvertTo(tval)); + REQUIRE(!itr->ConvertTo(uuidval)); + } + + SECTION("int64_t max") + { + auto itr = b.push_back(b.end(), "", 9223372036854775807); + + REQUIRE(!itr->ConvertTo(bval)); + REQUIRE((itr->ConvertTo(fval) && fval == 9223372036854775807.0)); + REQUIRE((itr->ConvertTo(ival) && ival == 9223372036854775807)); + REQUIRE((itr->ConvertTo(uval) && uval == 9223372036854775807)); + REQUIRE(!itr->ConvertTo(sval)); + REQUIRE(!itr->ConvertTo(tval)); + REQUIRE(!itr->ConvertTo(uuidval)); + } + + SECTION("greater than int64_t max and less than uint64_t max") + { + auto itr = b.push_back(b.end(), "", 9223372036854775808ull); + + REQUIRE(!itr->ConvertTo(bval)); + REQUIRE((itr->ConvertTo(fval) && fval == 9223372036854775808.0)); + REQUIRE(!itr->ConvertTo(ival)); + REQUIRE((itr->ConvertTo(uval) && uval == 9223372036854775808ull)); + REQUIRE(!itr->ConvertTo(sval)); + REQUIRE(!itr->ConvertTo(tval)); + REQUIRE(!itr->ConvertTo(uuidval)); + } + + SECTION("greater than int64_t max and less than uint64_t max as double") + { + auto itr = b.push_back(b.end(), "", 9223372036854777856.0); + + REQUIRE(!itr->ConvertTo(bval)); + REQUIRE((itr->ConvertTo(fval) && fval == 9223372036854777856.0)); + REQUIRE(!itr->ConvertTo(ival)); + REQUIRE((itr->ConvertTo(uval) && uval == 9223372036854777856ull)); + REQUIRE(!itr->ConvertTo(sval)); + REQUIRE(!itr->ConvertTo(tval)); + REQUIRE(!itr->ConvertTo(uuidval)); + } + + SECTION("uint64_t max") + { + auto itr = b.push_back(b.end(), "", 18446744073709551615ull); + + REQUIRE(!itr->ConvertTo(bval)); + REQUIRE((itr->ConvertTo(fval) && fval == 18446744073709551615.0)); + REQUIRE(!itr->ConvertTo(ival)); + REQUIRE((itr->ConvertTo(uval) && uval == 18446744073709551615ull)); + REQUIRE(!itr->ConvertTo(sval)); + REQUIRE(!itr->ConvertTo(tval)); + REQUIRE(!itr->ConvertTo(uuidval)); + } + + SECTION("greater than uint64_t max") + { + auto itr = b.push_back(b.end(), "", 18446744073709551616.0); + + REQUIRE(!itr->ConvertTo(bval)); + REQUIRE((itr->ConvertTo(fval) && fval == 18446744073709551615.0)); + REQUIRE(!itr->ConvertTo(ival)); + REQUIRE(!itr->ConvertTo(uval)); + REQUIRE(!itr->ConvertTo(sval)); + REQUIRE(!itr->ConvertTo(tval)); + REQUIRE(!itr->ConvertTo(uuidval)); + } + + SECTION("time") + { + auto now = std::chrono::system_clock::now(); + auto itr = b.push_back(b.end(), "", now); + + REQUIRE(!itr->ConvertTo(bval)); + REQUIRE(!itr->ConvertTo(fval)); + REQUIRE(!itr->ConvertTo(ival)); + REQUIRE(!itr->ConvertTo(uval)); + REQUIRE(!itr->ConvertTo(sval)); + REQUIRE((itr->ConvertTo(tval) && tval == now)); + REQUIRE(!itr->ConvertTo(uuidval)); + } + + SECTION("uuid") + { + UuidStruct uuid; + uuid_generate(uuid.Data); + + auto itr = b.push_back(b.end(), "", uuid); + + REQUIRE(!itr->ConvertTo(bval)); + REQUIRE(!itr->ConvertTo(fval)); + REQUIRE(!itr->ConvertTo(ival)); + REQUIRE(!itr->ConvertTo(uval)); + REQUIRE(!itr->ConvertTo(sval)); + REQUIRE(!itr->ConvertTo(tval)); + REQUIRE( + (itr->ConvertTo(uuidval) && + 0 == uuid_compare(uuidval.Data, uuid.Data))); + } +} +// int main() +// { +// JsonBuilder builder; +// builder.push_back(builder.end(), "field", 5l); +// builder.push_back(builder.end(), "String", "Grandes écoles"); +// builder.push_back( +// builder.end(), "CurrentTime", std::chrono::system_clock::now()); + +// JsonRenderer renderer; +// renderer.Pretty(true); + +// std::string_view output = renderer.Render(builder); + +// std::cout << output << std::endl; + +// return 0; +// } diff --git a/test/TestRenderer.cpp b/test/TestRenderer.cpp new file mode 100644 index 0000000..1895e9d --- /dev/null +++ b/test/TestRenderer.cpp @@ -0,0 +1,239 @@ +#include +#include + +#include +#include + +using namespace jsonbuilder; + +template +static void TestUInt(N n) +{ + unsigned const cchBuf = 24; + char buf1[cchBuf]; + char buf2[cchBuf]; + unsigned cch; + + memset(buf1, 1, sizeof(buf1)); + cch = JsonRenderUInt(n, buf1); + REQUIRE(cch < cchBuf); + for (unsigned i = cch + 1; i != cchBuf; i++) + { + REQUIRE(buf1[i] == 1); + } + std::snprintf( + buf2, std::size(buf2), "%llu", static_cast(n)); + for (unsigned i = 0; i <= cch; i++) + { + REQUIRE(buf1[i] == buf2[i]); + } +} + +template +static void TestInt(N n) +{ + unsigned const cchBuf = 24; + char buf1[cchBuf]; + char buf2[cchBuf]; + unsigned cch; + + memset(buf1, 1, sizeof(buf1)); + cch = JsonRenderInt(n, buf1); + REQUIRE(cch < cchBuf); + for (unsigned i = cch + 1; i != cchBuf; i++) + { + REQUIRE(buf1[i] == 1); + } + std::snprintf(buf2, std::size(buf2), "%lld", static_cast(n)); + for (unsigned i = 0; i <= cch; i++) + { + REQUIRE(buf1[i] == buf2[i]); + } +} + +template +static void TestFloat(N n) +{ + unsigned const cchBuf = 32; + char buf1[cchBuf]; + char buf2[cchBuf]; + unsigned cch; + + memset(buf1, 1, sizeof(buf1)); + cch = JsonRenderFloat(n, buf1); + REQUIRE(cch < cchBuf); + for (unsigned i = cch + 1; i != cchBuf; i++) + { + REQUIRE(buf1[i] == 1); + } + std::snprintf(buf2, std::size(buf2), "%.17g", static_cast(n)); + for (unsigned i = 0; i <= cch; i++) + { + REQUIRE(buf1[i] == buf2[i]); + } +} + +static void TestBool(bool n) +{ + unsigned const cchBuf = 6; + char buf1[cchBuf]; + char const* buf2 = n ? "true" : "false"; + memset(buf1, 1, sizeof(buf1)); + unsigned cch = JsonRenderBool(n, buf1); + REQUIRE(cch < cchBuf); + for (unsigned i = cch + 1; i != cchBuf; i++) + { + REQUIRE(buf1[i] == 1); + } + for (unsigned i = 0; i <= cch; i++) + { + REQUIRE(buf1[i] == buf2[i]); + } +} + +// TODO: Unused +// static void TestTime() {} + +template +static void TestUInts() +{ + SECTION("0") { TestUInt(0); } + SECTION("min") { TestUInt(std::numeric_limits::min()); } + SECTION("max") { TestUInt(std::numeric_limits::max()); } +} + +template +static void TestInts() +{ + SECTION("0") { TestInt(0); } + SECTION("min") { TestInt(std::numeric_limits::min()); } + SECTION("max") { TestInt(std::numeric_limits::max()); } +} + +template +static void TestFloats() +{ + SECTION("0") { TestFloat(0); } + SECTION("min") { TestFloat(std::numeric_limits::min()); } + SECTION("max") { TestFloat(std::numeric_limits::max()); } +} + +TEST_CASE("JsonRenderer values match printf", "[renderer]") +{ + SECTION("signed char") { TestInts(); } + SECTION("signed short") { TestInts(); } + SECTION("signed int") { TestInts(); } + SECTION("signed long") { TestInts(); } + SECTION("signed long long") { TestInts(); } + + SECTION("unsigned char") { TestUInts(); } + SECTION("unsigned short") { TestUInts(); } + SECTION("unsigned int") { TestUInts(); } + SECTION("unsigned long") { TestUInts(); } + SECTION("unsigned long long") { TestUInts(); } + + SECTION("float") { TestFloats(); } + SECTION("double") { TestFloats(); } + + SECTION("bool-false") { TestBool(false); } + SECTION("bool-true") { TestBool(true); } +} + +TEST_CASE("JsonRenderer JsonNull") +{ + unsigned const cchBuf = 24; + char buf1[cchBuf]; + char const* buf2 = "null"; + memset(buf1, 1, sizeof(buf1)); + unsigned cch = JsonRenderNull(buf1); + REQUIRE(cch < cchBuf); + for (unsigned i = cch + 1; i != cchBuf; i++) + { + REQUIRE(buf1[i] == 1); + } + for (unsigned i = 0; i <= cch; i++) + { + REQUIRE(buf1[i] == buf2[i]); + } +} + +using namespace std::string_view_literals; + +TEST_CASE("JsonRenderer JsonTime", "[renderer]") +{ + auto epoch = std::chrono::system_clock::from_time_t(0); + + char chars[39]; + memset(chars, 1, sizeof(chars)); + + unsigned cch = JsonRenderTime(epoch, chars); + REQUIRE(cch == strlen(chars)); + REQUIRE(chars == "1970-01-01T00:00:00.0000000Z"sv); +} + +TEST_CASE("JsonRenderer JsonUuid", "[renderer]") +{ + uuid_t uuid; + for (int i = 0; i < 16; i++) + { + uuid[i] = i; + } + + char chars[39]; + memset(chars, 1, sizeof(chars)); + + SECTION("Without braces") + { + unsigned cch = JsonRenderUuid(uuid, chars); + REQUIRE(cch == strlen(chars)); + REQUIRE(chars == "00010203-0405-0607-0809-0A0B0C0D0E0F"sv); + } + + SECTION("With braces") + { + unsigned cch = JsonRenderUuidWithBraces(uuid, chars); + REQUIRE(cch == strlen(chars)); + REQUIRE(chars == "{00010203-0405-0607-0809-0A0B0C0D0E0F}"sv); + } +} + +TEST_CASE("JsonRenderer full object", "[renderer]") +{ + JsonBuilder b; + + auto objItr = b.push_back(b.end(), "obj", JsonObject); + b.push_back(objItr, "str", "strval"); + b.push_back(objItr, "str2", "str2val"); + + auto arrItr = b.push_back(b.end(), "arr", JsonArray); + b.push_back(arrItr, "useless", 1); + b.push_back(arrItr, "useless2", 2); + + SECTION("Default renderer") + { + JsonRenderer renderer; + auto renderString = renderer.Render(b); + REQUIRE( + renderString == + R"({"obj":{"str":"strval","str2":"str2val"},"arr":[1,2]})"); + } + + SECTION("Pretty renderer") + { + JsonRenderer renderer; + renderer.Pretty(true); + auto renderString = renderer.Render(b); + REQUIRE( + renderString == + R"({ + "obj": { + "str": "strval", + "str2": "str2val" + }, + "arr": [ + 1, + 2 + ] +})"); + } +} \ No newline at end of file