Sleds/cppjson/json.cpp

740 lines
19 KiB
C++

///////////////////////////////////////////////////////////////////////////////
// Copyright(c) 2015, 2016 Sequence Logic.
///////////////////////////////////////////////////////////////////////////////
//
#include "json.h"
//#include <stdio.h>
#include <ctype.h>
#include <sstream>
#include <fstream>
#include <iomanip>
#include <locale>
#include <sstream>
#include <iostream>
using namespace sequencelogic;
#ifdef WIN32
#define STRDUP _strdup
#else
#define STRDUP strdup
#endif
namespace
{
unsigned int _sLineNo; // To keep track of the line number we are reading in, when loading JSON
};
////////////////////////////////////////////////////////////////////////
//
T_JSON T_JSON::nulljson("");
////////////////////////////////////////////////////////////////////////
// C/Dtor
T_JSON::T_JSON(const char* name, T_JSON* p) : name(NULL)
{
if (name)
setname(name);
else
setname("");
type = J_NULL;
parent = p;
}
T_JSON::~T_JSON()
{
for (unsigned int i = 0; i < data.size(); ++i)
delete data[i];
data.clear();
if (name != NULL)
::free(name);
if (type == J_STRING)
::free(string);
}
void T_JSON::copyfrom(const T_JSON& o)
{
if (type != J_ARRAY)
setname(o.getname());
settype(o.gettype());
switch(o.gettype())
{
case J_NUMBER:
number = o.number;
break;
case J_STRING:
string = static_cast<char *>(::malloc(::strlen(o.string)+1));
::memset(string, 0, ::strlen(o.string)+1);
::strcpy(string, o.string);
break;
case J_BOOLEAN:
boolean = o.boolean;
break;
case J_ARRAY:
case J_OBJECT:
{
for (int i = 0; i < o.getnumelements(); ++i)
{
T_JSON *pTmpObj = new T_JSON();
T_JSON *pRhs = o.get(i);
pTmpObj->copyfrom(*pRhs);
add(pTmpObj);
}
}
break;
case J_NULL:
break;
default:
break;
}
}
bool T_JSON::merge(const T_JSON& o)
{
if (type != J_OBJECT || o.type != J_OBJECT)
return false;
for (int i = 0; i < o.getnumitems(); i++)
{
if (o[i].gettype() == J_OBJECT)
{
T_JSON* t = get(o[i].getname());
if (!t)
add(new T_JSON(o[i]));
else
t->merge(o[i]);
}
else
update(o[i]); // what if it's an array?
}
return true;
}
bool T_JSON::operator == (const T_JSON& rhs) const
{
if (this == &rhs)
return true;
bool bRetVal = false;
if (gettype() == rhs.gettype())
{
// Check names...
if ((getname() != NULL) &&
(rhs.getname() != NULL) &&
(::strcmp(getname(), rhs.getname()) == 0))
{
// Check data...
switch (gettype())
{
case J_NUMBER:
bRetVal = (number == rhs.number);
break;
case J_STRING:
bRetVal = (::strcmp(string, rhs.string) == 0);
break;
case J_BOOLEAN:
bRetVal = (boolean == rhs.boolean);
break;
case J_ARRAY:
if (data.size() == rhs.data.size())
{
bRetVal = true;
for (unsigned int i = 0; bRetVal && (i < data.size()); ++i)
bRetVal = (*data[i] == *rhs.data[i]);
}
break;
case J_OBJECT:
if (data.size() == rhs.data.size())
{
bRetVal = true;
for (unsigned int i = 0; bRetVal && (i < data.size()); ++i)
{
const T_JSON *pObj = const_cast<T_JSON &>(rhs).search(data[i]->getname());
if (pObj != NULL)
bRetVal = (*data[i] == *pObj);
else
bRetVal = false;
}
}
break;
case J_NULL:
break;
default:
break;
}
}
}
return bRetVal;
}
void T_JSON::setname(const char *pName)
{
if (name == pName)
return; // If you pass getname() to setname(), it can seg fault.
if (name != NULL)
{
::free(name);
name = NULL;
}
if ((pName != NULL) && (pName[0] != '\0'))
{
size_t nLen = ::strlen(pName) + 1;
name = static_cast<char *>(::malloc(nLen));
::memset(name, 0, nLen);
::strcpy(name, pName);
}
}
T_JSON* T_JSON::search(const char *pNameToFind)
{
if ((pNameToFind == NULL) || (pNameToFind[0] == '\0'))
return NULL;
T_JSON *pRetVal = NULL;
for (unsigned int i = 0; (pRetVal == NULL) && (i < data.size()); ++i)
{
T_JSON *pTmp = data.at(i);
if ((pTmp->name != NULL) && (pTmp->name[0] != '\0') && (::strcmp(pTmp->name, pNameToFind) == 0))
pRetVal = pTmp;
}
return pRetVal;
}
void T_JSON::erase(const char *pKey)
{
if ((pKey != NULL) && (pKey[0] != '\0'))
{
T_JSON *pMember = NULL;
for (IntData::iterator iter = data.begin(); iter != data.end(); ++iter)
{
const char *pName = (*iter)->getname();
if ((pName != NULL) && (pName[0] != '\0') && (::strcmp((*iter)->getname(), pKey) == 0))
{
pMember = *iter;
data.erase(iter);
delete pMember;
break;
}
}
}
}
////////////////////////////////////////////////////////////////////////
// Get ready to change the type
void T_JSON::free()
{
if (type == J_STRING)
::free(string);
else if (type == J_ARRAY || type == J_OBJECT)
{
for (unsigned int i = 0; i < data.size(); ++i)
delete data[i];
data.clear();
}
string = NULL;
type = J_NULL;
}
void T_JSON::settype(JSONTYPE t)
{
free();
type = t;
}
////////////////////////////////////////////////////////////////////////
// Error handling. For the moment I don't do much.
void T_JSON::error(const char* errorstr) const
{
fprintf(stderr, "JSON ERROR on object %s - %s\n", getname(), errorstr);
}
void T_JSON::error(const char* errorstr, FILE* f) const
{
fprintf(stderr, "JSON ERROR on object %s - %s\n", getname(), errorstr);
fprintf(stderr, " at about this point in the file: %s",
fgets((char*)alloca(J_MAXSTRLEN), J_MAXSTRLEN, f));
}
////////////////////////////////////////////////////////////////////////
// Set internals
void T_JSON::setnumber(double d)
{
settype(J_NUMBER);
number = d;
}
void T_JSON::setnumber(int64_t i)
{
settype(J_NUMBER);
number = static_cast<double>(i);
}
void T_JSON::setstring(const char* s)
{
settype(J_STRING);
string = ::STRDUP(s);
}
void T_JSON::setboolean(bool b)
{
settype(J_BOOLEAN);
boolean = b;
}
void T_JSON::add(T_JSON* j)
{
j->parent = this;
if (type == J_NULL)
settype(J_OBJECT);
checktype(J_OBJECT, J_ARRAY);
data.push_back(j);
}
////////////////////////////////////////////////////////////////////////
// I/O
// Format a string replacing bad characters with \ escapes
static std::string format(const char* source)
{
const char* s = source;
size_t found = 0;
for (; *s; s++)
found += (*s < ' ' || *s > 0x7e || *s == '\\' || *s == '\"') ? 1 : 0;
if (!found)
return source;
// gotta format shit
s = source;
size_t nBufLen = ::strlen(source) + 10 + found * 5;
char* dest = static_cast<char *>(::alloca(nBufLen));
::memset(dest, 0, nBufLen);
for (char* d = dest; *s; s++)
{
switch(*s)
{
case '\\': *d++ = '\\'; *d++ = '\\'; break;
case '"': *d++ = '\\'; *d++ = '"'; break;
case '\a': *d++ = '\\'; *d++ = 'a'; break;
case '\b': *d++ = '\\'; *d++ = 'b'; break;
case '\f': *d++ = '\\'; *d++ = 'f'; break;
case '\n': *d++ = '\\'; *d++ = 'n'; break;
case '\r': *d++ = '\\'; *d++ = 'r'; break;
case '\t': *d++ = '\\'; *d++ = 't'; break;
case '\v': *d++ = '\\'; *d++ = 'v'; break;
default:
if (*s && (*s < 0x20 || *s > 0x7e))
{
// Write out actual bytes, as if it's UTF8
unsigned short nNumBytes = 0;
if ((*s & 0xfe) == 0xfe)
nNumBytes = 7;
else if ((*s & 0xfc) == 0xfc)
nNumBytes = 6;
else if ((*s & 0xf8) == 0xf8)
nNumBytes = 5;
else if ((*s & 0xf0) == 0xf0)
nNumBytes = 4;
else if ((*s & 0xe0) == 0xe0)
nNumBytes = 3;
else if ((*s & 0xc0) == 0xc0)
nNumBytes = 2;
else
{
// JSON doesn't support the '\xZZ' escape sequence. Use UNICODE instead. http://json.org/
sprintf(d, "\\u%04x", (unsigned)(unsigned char)(*s));
int nLen = (int)::strlen(d);
d += nLen;
}
if (nNumBytes > 0)
{
::memcpy(d, s, nNumBytes);
d += nNumBytes;
s += nNumBytes-1;
}
}
else
*d++ = *s;
break;
}
#ifndef NODEBUG
if (d >= dest + nBufLen - 5)
{
fprintf(stderr, "FATAL ERROR: buffer overrun in JSON string formatting"
"\nwhile attempting to encode string '%s'\n", source);
exit(1);
}
#endif
}
std::string retVal(dest);
return retVal;
}
////////////////////////////////////////////////////////////////////////
// Dump the JSON to a file.
bool T_JSON::print(std::ostream &out, int indent /*= 4*/, bool bPrettyPrint /*= false*/) const
{
bool bRetVal = true;
int nActualIndent = ((bPrettyPrint) ? indent : 0);
if (parent && parent->type != J_ARRAY && getname() != NULL && getname()[0] != '\0')
out << std::string(nActualIndent, ' ') << "\"" << format(getname()) << "\":";
else
out << std::string(nActualIndent, ' ');
switch (type)
{
case J_NUMBER:
{
if (static_cast<long long>(number) == number)
out << /*std::setw(12) << */ static_cast<long long>(number);
else
out << /*std::setw(12) <<*/ number;
}
break;
case J_STRING:
out << "\"" << format(string) << "\"";
break;
case J_BOOLEAN:
out << ((boolean) ? "true" : "false");
break;
case J_ARRAY:
case J_OBJECT:
out << ((type == J_ARRAY) ? "[" : "{");
if (bPrettyPrint)
out << "\n";
for (int i = 0; i < getnumelements(); i++)
{
bRetVal = get(i)->print(out, nActualIndent+3, bPrettyPrint);
if (i < getnumelements()-1)
out << ",";
if (bPrettyPrint)
out << "\n";
}
out << std::string(nActualIndent, ' ') << ((type == J_ARRAY) ? "]" : "}");
break;
case J_NULL:
out << "null";
break;
}
return bRetVal;
}
////////////////////////////////////////////////////////////////////////
// Generate a string version of the JSON object
std::string T_JSON::toString(int indent, bool pretty) const
{
std::stringstream jstr;
print(jstr, indent, pretty);
return jstr.str();
}
////////////////////////////////////////////////////////////////////////
// get up to an ending ", processing stupid c-style \ escapes
static char* loadstring(JSONStream &stream)
{
char* buf = (char*) alloca(T_JSON::J_MAXSTRLEN);
char* s = buf;
char c = '"';
while ((c = stream.getChar()) != EOF)
{
char c1 = '\n', c2 = '\n';
if (c == '\\')
{
c = stream.getChar();
switch(c)
{
case '\\': c = '\\'; break;
case 'a': c = '\a'; break;
case 'b': c = '\b'; break;
case 'f': c = '\f'; break;
case 'n': c = '\n'; break;
case 'r': c = '\r'; break;
case 't': c = '\t'; break;
case 'v': c = '\v'; break;
case '/': c = '/'; break;
case '"': c = '\"'; break;
case 'x':
c1 = stream.getChar();
if (c1 > 'a') c1 -= 'a' - 10;
else if (c1 > 'A') c1 -= 'A' - 10;
else c1 -= '0';
c2 = stream.getChar();
if ((std::isdigit(c2, std::locale()) ||
(('A' <= c2) && (c2 <= 'F')) ||
(('a' <= c2) && (c2 <= 'f'))))
{
if (c2 > 'a') c2 -= 'a' - 10;
else if (c2 > 'A') c2 -= 'A' - 10;
else c2 -= '0';
c = c1 << 4 | c2;
}
else
{
stream.putChar(c2);
c = c1;
}
break;
case '0':
case '1':
c1 = stream.getChar();
c1 -= '0';
c2 = stream.getChar();
c2 -= '0';
c = ((c - '0') << 6) | (c1 << 3) | c2;
break;
default:
{
std::cerr << "Invalid escape character '" << c << "' in JSON file at line " << _sLineNo << std::endl;
c = stream.getChar();
}
break;
}
}
else if (c == '"')
break;
*s++ = c;
}
if (c == EOF)
return NULL;
*s = '\0';
return ::STRDUP(buf);
}
// find the next non-space in the file.
static int skipspaces(JSONStream &stream, unsigned int &nLineNo)
{
for (;;)
{
char c = EOF;
c = stream.getChar();
if (c == '\n')
++nLineNo;
if (c == '/') // allow '//' followed by comment.
{
char newc = EOF;
newc = stream.getChar();
if (newc == '/')
{
while (newc != EOF && newc != '\n')
newc = stream.getChar();
if (newc == '\n')
++nLineNo;
continue;
}
if (newc != EOF)
stream.putChar(newc);
}
if (c == EOF || !isspace(static_cast<unsigned char>(c)))
return c;
}
}
////////////////////////////////////////////////////////////////////////
bool T_JSON::load(JSONStream &stream)
{
char c = skipspaces(stream, _sLineNo);
// get the name, if there is one
if (c == '"')
{
char* s = loadstring(stream);
if (!s)
{
std::stringstream msg;
msg << "Unterminated name string in JSON file at line " << _sLineNo;
error(msg.str().c_str());
return false;
}
c = skipspaces(stream, _sLineNo);
if (c != ':')
{
stream.putChar(c);
settype(J_STRING);
string = s;
return true;
}
setname(s);
::free(s);
c = skipspaces(stream, _sLineNo);
}
switch(c)
{
case '{':
case '[':
{
int expect = (c == '{' ? '}' : ']');
settype(c == '{' ? J_OBJECT : J_ARRAY);
c = skipspaces(stream, _sLineNo);
if (c == expect)
return true;
stream.putChar(c);
for(;;)
{
T_JSON* j = new T_JSON("", this);
if (!j->load(stream))
{
delete j;
return false;
}
add(j);
c = skipspaces(stream, _sLineNo);
if (c == expect)
break;
else if (c != ',')
{
std::stringstream msg;
msg << "Expected comma (',') or right " << ((expect == '}') ? "brace" : "bracket") << " at line " << _sLineNo;
error(msg.str().c_str());
return false;
}
}
}
break;
case 't': case 'T':
{
char c1 = 'z', c2 = 'z', c3 = 'z';
c1 = stream.getChar();
c2 = stream.getChar();
c3 = stream.getChar();
if ((c1 != 'r' && c1 != 'R') ||
(c2 != 'u' && c2 != 'U') ||
(c3 != 'e' && c3 != 'E'))
goto broken;
}
settype(J_BOOLEAN);
boolean = true;
break;
case 'f': case 'F':
{
char c1 = 'z', c2 = 'z', c3 = 'z', c4 = 'z';
c1 = stream.getChar();
c2 = stream.getChar();
c3 = stream.getChar();
c4 = stream.getChar();
if ((c1 != 'a' && c1 != 'A') ||
(c2 != 'l' && c2 != 'L') ||
(c3 != 's' && c3 != 'S') ||
(c4 != 'e' && c4 != 'E'))
goto broken;
}
settype(J_BOOLEAN);
boolean = false;
break;
break;
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
case '+': case '-':
{
stream.putChar(c);
settype(J_NUMBER);
number = stream.getNum();
}
break;
case '"':
settype(J_STRING);
if (!(string = loadstring(stream)))
{
std::stringstream msg;
msg << "Unterminated string at line " << _sLineNo;
error(msg.str().c_str());
return false;
}
break;
case 'n': case 'N':
{
char c1 = 'z', c2 = 'z', c3 = 'z';
c1 = stream.getChar();
c2 = stream.getChar();
c3 = stream.getChar();
if ((c1 != 'u' && c1 != 'U') ||
(c2 != 'l' && c2 != 'L') ||
(c3 != 'l' && c3 != 'L'))
goto broken;
}
break;
case ',':
// This is really an error. You shouldn't have a key with no value at all.
stream.putChar(c);
break;
case EOF:
//
error("JSON file is truncated.");
return false;
default:
broken:
error("JSON file format not valid");
return false;
}
return true;
}
////////////////////////////////////////////////////////////////////////
bool T_JSON::store(const char* filename, bool bPrettyPrint /*= true*/) const
{
bool bRetVal = false;
std::ofstream outFile (filename, std::ios_base::out|std::ios_base::trunc);
if (!outFile.bad())
{
bRetVal = print(outFile, 0, bPrettyPrint);
outFile.close();
}
return bRetVal;
}
bool T_JSON::load(const char* filename)
{
_sLineNo = 1;
FILE *pFile = ::fopen(filename, "r");
if (!pFile)
return false;
bool bRetVal = load(pFile);
::fclose(pFile);
return bRetVal;
}
bool T_JSON::load(FILE* f)
{
JSONStream stream(f);
return load(stream);
}
bool T_JSON::loadFromString(const char *pJSONStr)
{
_sLineNo = 1;
bool bRetVal = false;
if ((pJSONStr != NULL) && (pJSONStr[0] != '\0'))
{
JSONStream stream(pJSONStr);
bRetVal = load(stream);
}
return bRetVal;
}