AzureSphereSquirrel/HLCore/json.cpp (662 lines of code) (raw):
/* Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the MIT License. */
#include "json.h"
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <applibs/log.h>
#include "squirrel/include/sqstdblob.h"
#define JSON_MAX_ENCODE_DEPTH 32 ///< Set the maximum encode depth to prevent against abuse/cyclical references.
#define JSON_MAX_TOKENS 512 ///< Set the maximum number of JSON tokens that may be decoded (avoids a malloc).
#define JSON_INITIAL_DECODE_SIZE 50 ///< Set the initial storage size of the decoded JSON string (a point of optimisation).
// Static Methods
//---------------
/// Registers the JSON class with Squirrel as a global (stored in the root table) singleton.
/// \param vm the instance of the VM to use.
/// \param name the name by which the class will be accessible from within Squirrel.
/// \returns a pointer to the singleton instance instantiated and placed within the root table.
JSON* JSON::registerWithSquirrelAsGlobal(HSQUIRRELVM vm, const char* name)
{
SquirrelCppHelper::DelegateFunction delegateFunctions[2];
delegateFunctions[0] = SquirrelCppHelper::DelegateFunction("decode", &JSON::SQUIRREL_METHOD_NAME(decode));
delegateFunctions[1] = SquirrelCppHelper::DelegateFunction("encode", &JSON::SQUIRREL_METHOD_NAME(encode));
return SquirrelCppHelper::registerClassAsGlobal<JSON>(vm, name, delegateFunctions, 2);
}
/// Grows the size of an sq_malloc allocated SQChar array to match at least the requiredSize.
/// Will overgrow proportionally to reduce the number of sq_reallocs.
/// \param storage a reference to the storage array to be grown.
/// \param storageSize a reference to the current size (in bytes) of the storage to be grown.
/// \param requiredSize the minimum size required to grow the storage to.
void JSON::growStorage(SQChar *&storage, SQInteger &storageSize, SQInteger requiredSize)
{
if(storageSize < requiredSize)
{
requiredSize = requiredSize + (requiredSize >> 1);
storage = (SQChar *)sq_realloc(storage, storageSize, requiredSize);
storageSize = requiredSize;
}
}
/// Shrinks the size of an sq_malloc allocated SQChar array to match the finalSize.
/// \param storage a reference to the storage array to be shrunk to fit.
/// \param storageSize the current size (in bytes) of the storage to be shrunk to fit.
/// \param requiredSize the size required to fit the storage to.
void JSON::fitStorage(SQChar *&storage, SQInteger storageSize, SQInteger finalSize)
{
storage = (SQChar *)sq_realloc(storage, storageSize, finalSize);
}
// Squirrel Methods
//-----------------
/// Decodes a strict JSON string into a Squirrel object.
/// \param json the strict JSON to be decoded.
/// \returns the decoded Squirrel object.
/// \throws an error message if the JSON string could not be decoded.
SQUIRREL_METHOD_IMPL(JSON, decode)
{
// Validate the number and type of parameters
int types[] = {OT_STRING};
if(SQ_FAILED(SquirrelCppHelper::checkParameterTypes(vm, 1, 1, types)))
{
return SQ_ERROR;
}
// Retrieve the JSON string to be decoded
SQChar* jsonString;
SQInteger jsonStringLen;
sq_getstringandsize(vm, 2, (const SQChar**)&jsonString, &jsonStringLen);
// Parse the JSON string
jsmn_init(&jsmn);
int result = jsmn_parse(&jsmn, jsonString, jsonStringLen, tokenBuffer, JSON_MAX_TOKENS);
if(result < 0)
{
return sq_throwerror(vm, "Unable to parse JSON");
}
// Construct a Squirrel object on the stack and insert the JSON entries
if(SQ_FAILED(parseToken(vm, jsonString, tokenBuffer, 0)))
{
return sq_throwerror(vm, "Unable to convert JSON");
}
// Return the Squirrel object from the top of the stack
return 1;
}
/// Encodes a Squirrel object into a strict JSON string.
/// \param value the Squirrel object to be encoded.
/// \returns the encoded JSON string.
/// \throws an error message if the JSON string could not be encoded.
SQUIRREL_METHOD_IMPL(JSON, encode)
{
// Validate the number and type of parameters
int types[] = {OT_TABLE|OT_ARRAY};
if(SQ_FAILED(SquirrelCppHelper::checkParameterTypes(vm, 1, 1, types)))
{
return SQ_ERROR;
}
// Retrieve the Squirrel object to be encoded
HSQOBJECT object;
sq_resetobject(&object);
sq_getstackobj(vm, 2, &object);
// Prepare efficiently growable storage for the JSON string
SQChar *jsonString = (SQChar*)sq_malloc(JSON_INITIAL_DECODE_SIZE);
SQInteger jsonStringStorageSize = JSON_INITIAL_DECODE_SIZE;
SQInteger jsonStringSize = 0;
if(SQ_FAILED(encodeObject(vm, object, jsonString, jsonStringStorageSize, jsonStringSize)))
{
sq_free(jsonString, jsonStringStorageSize);
return SQ_ERROR;
}
// Grow storage to account for '\0'
growStorage(jsonString, jsonStringStorageSize, jsonStringSize+1);
// NULL terminate the string
jsonString[jsonStringSize] = '\0';
// Copy and push the the JSON string onto the stack as a Squirrel String
sq_pushstring(vm, jsonString, jsonStringSize);
// Free the JSON string
sq_free(jsonString, jsonStringStorageSize);
// Return the JSON string from the top of the stack
return 1;
}
// Methods
//--------
/// Parses recursively a JSON token produced by JSMN decode into a Squirrel object.
/// \param vm the instance of the VM to use.
/// \param jsonString the JSON string being parsed (used for data extraction).
/// \param tokens a list of parsed tokens.
/// \param tokenIndex the index of the currently parsing token in the tokens list.
/// \returns SQ_OK or SQ_ERROR if the JSON string could not be decoded.
/// \throws an error message if the JSON string could not be decoded.
SQInteger JSON::parseToken(HSQUIRRELVM vm, char *jsonString, jsmntok_t tokens[], unsigned int tokenIndex)
{
jsmntok_t *token = &tokens[tokenIndex];
switch(token->type)
{
case jsmntype_t::JSMN_OBJECT: return parseObject(vm, jsonString, tokens, tokenIndex);
case jsmntype_t::JSMN_ARRAY: return parseArray(vm, jsonString, tokens, tokenIndex);
case jsmntype_t::JSMN_STRING: return parseString(vm, jsonString, tokens, tokenIndex);
case jsmntype_t::JSMN_PRIMITIVE:
switch(jsonString[token->start])
{
case 't': case 'f': return parseBool(vm, jsonString, tokens, tokenIndex);
case 'n': return parseNull(vm, jsonString, tokens, tokenIndex);
case '-': case '0': case '1': case '2':
case '3': case '4': case '5': case '6':
case '7': case '8': case '9': return parseNumber(vm, jsonString, tokens, tokenIndex);
default: return sq_throwerror(vm, "Unknown primitive token");
}
break;
case jsmntype_t::JSMN_UNDEFINED:
default: return sq_throwerror(vm, "Unknown token type");
}
}
/// Parses a JSON object token produced by JSMN decode into a Squirrel table.
/// \param vm the instance of the VM to use.
/// \param jsonString the JSON string being parsed (used for data extraction).
/// \param tokens a list of parsed tokens.
/// \param tokenIndex the index of the currently parsing token in the tokens list.
/// \returns SQ_OK or SQ_ERROR if the JSON object token could not be decoded.
/// \throws an error message if the JSON object token could not be decoded.
SQInteger JSON::parseObject(HSQUIRRELVM vm, char *jsonString, jsmntok_t tokens[], unsigned int tokenIndex)
{
jsmntok_t *token = &tokens[tokenIndex];
int numberOfKeyPairs = token->size;
int childTokenIndexStart = tokenIndex + 1;
int childTokenIndexEnd = childTokenIndexStart + numberOfKeyPairs;
sq_newtableex(vm, numberOfKeyPairs);
for( int i = childTokenIndexStart; i < childTokenIndexEnd; i += 2)
{
if(SQ_FAILED(parseToken(vm, jsonString, tokens, i))) { return sq_throwerror(vm, "Unable to parse object token key"); }
if(SQ_FAILED(parseToken(vm, jsonString, tokens, i+1))) { return sq_throwerror(vm, "Unable to parse object token value"); }
sq_createslot(vm, -3);
}
return SQ_OK;
}
/// Parses a JSON array token produced by JSMN decode into a Squirrel array.
/// \param vm the instance of the VM to use.
/// \param jsonString the JSON string being parsed (used for data extraction).
/// \param tokens a list of parsed tokens.
/// \param tokenIndex the index of the currently parsing token in the tokens list.
/// \returns SQ_OK or SQ_ERROR if the JSON array token could not be decoded.
/// \throws an error message if the JSON array token could not be decoded.
SQInteger JSON::parseArray(HSQUIRRELVM vm, char *jsonString, jsmntok_t tokens[], unsigned int tokenIndex)
{
jsmntok_t *token = &tokens[tokenIndex];
int numberOfElements = token->size;
int childTokenIndexStart = tokenIndex + 1;
int childTokenIndexEnd = childTokenIndexStart + numberOfElements;
sq_newarray(vm, numberOfElements);
for( int i = childTokenIndexStart, u = 0; i < childTokenIndexEnd; ++i, ++u)
{
if(SQ_FAILED(parseToken(vm, jsonString, tokens, i))) { return sq_throwerror(vm, "Unable to parse array token element"); }
sq_arrayinsert(vm, -2, u);
}
return SQ_OK;
}
/// Parses a JSON string token produced by JSMN decode into a Squirrel string, unescaping characters as required.
/// \param vm the instance of the VM to use.
/// \param jsonString the JSON string being parsed (used for data extraction).
/// \param tokens a list of parsed tokens.
/// \param tokenIndex the index of the currently parsing token in the tokens list.
/// \returns SQ_OK or SQ_ERROR if the JSON string token could not be decoded.
/// \throws an error message if the JSON string token could not be decoded.
/// \todo Add support for unicode and unicode \u parsing
SQInteger JSON::parseString(HSQUIRRELVM vm, char *jsonString, jsmntok_t tokens[], unsigned int tokenIndex)
{
jsmntok_t *token = &tokens[tokenIndex];
char *string = &jsonString[token->start];
int stringLen = token->end - token->start;
int finalStringLen = stringLen;
for(int i = 0; i < stringLen-1; ++i)
{
if(string[i] == '\\')
{
switch(string[i+1])
{
case '"': case '\\': case '/':
string[i] = 255;
--finalStringLen;
++i;
break;
case 'b':
string[i] = 255;
string[i+1] = '\b';
--finalStringLen;
++i;
break;
case 'f':
string[i] = 255;
string[i+1] = '\f';
--finalStringLen;
++i;
break;
case 'n':
string[i] = 255;
string[i+1] = '\n';
--finalStringLen;
++i;
break;
case 'r':
string[i] = 255;
string[i+1] = '\r';
--finalStringLen;
++i;
break;
case 't':
string[i] = 255;
string[i+1] = '\t';
--finalStringLen;
++i;
break;
case 'u':
unsigned int byteCode;
if(stringLen - i == 6)
{
byteCode = strtol(&string[i+2], nullptr, 16);
}
else if(stringLen - i > 6)
{
char tempChar = string[i+6];
string[i+6] = '\0';
byteCode = strtol(&string[i+2], nullptr, 16);
string[i+6] = tempChar;
}
else
{
return sq_throwerror(vm, "Unable to parse string token \\u");
}
if(byteCode >= 256)
{
return sq_throwerror(vm, "Unable to parse string token \\u, sizes above 1Byte not yet supported");
}
string[i] = 255;
string[i+1] = byteCode;
string[i+2] = 255;
string[i+3] = 255;
string[i+4] = 255;
string[i+5] = 255;
finalStringLen -= 5;
break;
default: return sq_throwerror(vm, "Unable to parse string token, unknown escape sequence");;
}
}
}
for(int i = 0, u = 0; i < stringLen; ++i)
{
if(string[i] == 255)
{
continue;
}
string[u++] = string[i];
}
sq_pushstring(vm, string, finalStringLen);
return SQ_OK;
}
/// Parses a JSON bool token produced by JSMN decode into a Squirrel bool.
/// \param vm the instance of the VM to use.
/// \param jsonString the JSON string being parsed (used for data extraction).
/// \param tokens a list of parsed tokens.
/// \param tokenIndex the index of the currently parsing token in the tokens list.
/// \returns SQ_OK or SQ_ERROR if the JSON bool token could not be decoded.
/// \throws an error message if the JSON bool token could not be decoded.
SQInteger JSON::parseBool(HSQUIRRELVM vm, char *jsonString, jsmntok_t tokens[], unsigned int tokenIndex)
{
jsmntok_t *token = &tokens[tokenIndex];
sq_pushbool(vm, jsonString[token->start] == 't' ? true : false);
return SQ_OK;
}
/// Parses a JSON null token produced by JSMN decode into a Squirrel null.
/// \param vm the instance of the VM to use.
/// \param jsonString the JSON string being parsed (used for data extraction).
/// \param tokens a list of parsed tokens.
/// \param tokenIndex the index of the currently parsing token in the tokens list.
/// \returns SQ_OK or SQ_ERROR if the JSON null token could not be decoded.
/// \throws an error message if the JSON null token could not be decoded.
SQInteger JSON::parseNull(HSQUIRRELVM vm, char *jsonString, jsmntok_t tokens[], unsigned int tokenIndex)
{
sq_pushnull(vm);
return SQ_OK;
}
/// Parses a JSON number token produced by JSMN decode into a Squirrel integer or float.
/// \param vm the instance of the VM to use.
/// \param jsonString the JSON string being parsed (used for data extraction).
/// \param tokens a list of parsed tokens.
/// \param tokenIndex the index of the currently parsing token in the tokens list.
/// \returns SQ_OK or SQ_ERROR if the JSON number token could not be decoded.
/// \throws an error message if the JSON number token could not be decoded.
SQInteger JSON::parseNumber(HSQUIRRELVM vm, char *jsonString, jsmntok_t tokens[], unsigned int tokenIndex)
{
jsmntok_t *token = &tokens[tokenIndex];
const char *string = &jsonString[token->start];
int stringLen = token->end - token->start;
for(int i = 0; i < stringLen; ++i)
{
if(!isdigit(string[i]))
{
sq_pushfloat(vm, strtof(string, nullptr));
return SQ_OK;
}
}
sq_pushinteger(vm, strtol(string, nullptr, 10));
return SQ_OK;
}
/// Recursively encodes a Squirrel object into a strict JSON string.
/// \param vm the instance of the VM to use.
/// \param object the Squirrel object to be encoded.
/// \param jsonString the storage for the encoded JSON string (must be first initialied by sq_malloc and ultimately freed with sq_free by the user).
/// \param jsonStringStorageSize the size of the encoded JSON string storage.
/// \param jsonStringSize the size of the writen/decoded JSON string so far.
/// \param depth the current recursion depth (leave blank on first call).
/// \returns SQ_OK or SQ_ERROR if the Squirrel object could not be encoded.
/// \throws an error message if the Squirrel object could not be encoded.
SQInteger JSON::encodeObject(HSQUIRRELVM vm, HSQOBJECT object, SQChar *&jsonString, SQInteger &jsonStringStorageSize, SQInteger &jsonStringSize, SQInteger depth)
{
// Determine if we've recursed too deep,
// we're likely in a cicular reference and risk a stack overflow.
if(depth > JSON_MAX_ENCODE_DEPTH)
{
return sq_throwerror(vm, "Maximum encode depth reached");
}
// Determine the type of the object to encode
switch(sq_type(object))
{
case OT_TABLE:
case OT_CLASS:
if(SQ_FAILED(encodeClassTable(vm, object, jsonString, jsonStringStorageSize, jsonStringSize, depth)))
{
return SQ_ERROR;
}
break;
case OT_ARRAY:
if(SQ_FAILED(encodeArray(vm, object, jsonString, jsonStringStorageSize, jsonStringSize, depth)))
{
return SQ_ERROR;
}
break;
case OT_STRING:
if(SQ_FAILED(encodeString(vm, object, jsonString, jsonStringStorageSize, jsonStringSize, depth)))
{
return SQ_ERROR;
}
break;
case OT_INTEGER:
case OT_FLOAT:
if(SQ_FAILED(encodeNumber(vm, object, jsonString, jsonStringStorageSize, jsonStringSize, depth)))
{
return SQ_ERROR;
}
break;
case OT_BOOL:
if(SQ_FAILED(encodeBool(vm, object, jsonString, jsonStringStorageSize, jsonStringSize, depth)))
{
return SQ_ERROR;
}
break;
case OT_NULL:
if(SQ_FAILED(encodeNull(vm, object, jsonString, jsonStringStorageSize, jsonStringSize, depth)))
{
return SQ_ERROR;
}
break;
case OT_INSTANCE:
if(SQ_FAILED(encodeInstance(vm, object, jsonString, jsonStringStorageSize, jsonStringSize, depth)))
{
return SQ_ERROR;
}
break;
default: return sq_throwerror(vm, "Unserializable object encountered");
}
return SQ_OK;
}
/// Recursively encodes a Squirrel class|table into a strict JSON string.
/// \param vm the instance of the VM to use.
/// \param object the Squirrel object to be encoded.
/// \param jsonString the storage for the encoded JSON string (must be first initialied by sq_malloc and ultimately freed with sq_free by the user).
/// \param jsonStringStorageSize the size of the encoded JSON string storage.
/// \param jsonStringSize the size of the writen/decoded JSON string so far.
/// \param depth the current recursion depth (leave blank on first call).
/// \returns SQ_OK or SQ_ERROR if the Squirrel class|table could not be encoded.
/// \throws an error message if the Squirrel class|table could not be encoded.
SQInteger JSON::encodeClassTable(HSQUIRRELVM vm, HSQOBJECT object, SQChar *&jsonString, SQInteger &jsonStringStorageSize, SQInteger &jsonStringSize, SQInteger depth)
{
// Grow storage to account for '{'
growStorage(jsonString, jsonStringStorageSize, jsonStringSize+1);
jsonString[jsonStringSize++] = '{';
// Push the table|class and a blank iterator to the stack
sq_pushobject(vm, object);
sq_pushnull(vm);
// Take a snapshot of the jsonStringSize to see if there were properties in the table|class
SQInteger outputSizeSnapshot = jsonStringSize;
// Iterate over each entry within the table|class (will push key and value onto the stack)
while(SQ_SUCCEEDED(sq_next(vm, -2)))
{
// Retrieve the key
const SQChar* key;
SQInteger keySize;
sq_getstringandsize(vm, -2, &key, &keySize); // Ignore the return value, we know it's a string
// Retrieve the value
HSQOBJECT value;
sq_resetobject(&value);
sq_getstackobj(vm, -1, &value);
// Pop both the key and value from the stack before we recurse to reduce stack depth impact
sq_pop(vm, 2);
// Ignore the entry if it refers to a function (we're only serializing properties)
if(!sq_isfunction(value))
{
// Grow storage to account for '"key":'
growStorage(jsonString, jsonStringStorageSize, jsonStringSize+3+keySize);
// Encode the key
jsonString[jsonStringSize++] = '"';
memcpy(&jsonString[jsonStringSize], key, keySize);
jsonStringSize += keySize;
jsonString[jsonStringSize++] = '"';
jsonString[jsonStringSize++] = ':';
// Recurse to encode the value
if(SQ_FAILED(encodeObject(vm, value, jsonString, jsonStringStorageSize, jsonStringSize, depth+1)))
{
return SQ_ERROR;
}
// Grow storage to account for ','
growStorage(jsonString, jsonStringStorageSize, jsonStringSize+1);
jsonString[jsonStringSize++] = ',';
}
}
// Pop the iterator and object from the stack
sq_pop(vm, 2);
// If there were entries, place the closing '}' one space back to erase the trailing comma
if(outputSizeSnapshot != jsonStringSize)
{
jsonString[jsonStringSize-1] = '}';
}
else
{
// Grow storage to account for '}'
growStorage(jsonString, jsonStringStorageSize, jsonStringSize+1);
jsonString[jsonStringSize++] = '}';
}
return SQ_OK;
}
/// Recursively encodes a Squirrel array into a strict JSON string.
/// \param vm the instance of the VM to use.
/// \param object the Squirrel object to be encoded.
/// \param jsonString the storage for the encoded JSON string (must be first initialied by sq_malloc and ultimately freed with sq_free by the user).
/// \param jsonStringStorageSize the size of the encoded JSON string storage.
/// \param jsonStringSize the size of the writen/decoded JSON string so far.
/// \param depth the current recursion depth (leave blank on first call).
/// \returns SQ_OK or SQ_ERROR if the Squirrel array could not be encoded.
/// \throws an error message if the Squirrel array could not be encoded.
SQInteger JSON::encodeArray(HSQUIRRELVM vm, HSQOBJECT object, SQChar *&jsonString, SQInteger &jsonStringStorageSize, SQInteger &jsonStringSize, SQInteger depth)
{
// Grow storage to account for '['
growStorage(jsonString, jsonStringStorageSize, jsonStringSize+1);
jsonString[jsonStringSize++] = '[';
// Push the array and a blank iterator to the stack
sq_pushobject(vm, object);
sq_pushnull(vm);
// Take a snapshot of the jsonStringSize to see if there were elements in the array
SQInteger outputSizeSnapshot = jsonStringSize;
// Iterate over each entry within the array (will push index and value onto the stack)
while(SQ_SUCCEEDED(sq_next(vm, -2)))
{
// Retrieve the value (we're ignoring the index)
HSQOBJECT value;
sq_resetobject(&value);
sq_getstackobj(vm, -1, &value);
// Pop both the index and value from the stack before we recurse to reduce stack depth impact
sq_pop(vm, 2);
// Recurse to encode the value
if(SQ_FAILED(encodeObject(vm, value, jsonString, jsonStringStorageSize, jsonStringSize, depth+1)))
{
return SQ_ERROR;
}
// Grow storage to account for ','
growStorage(jsonString, jsonStringStorageSize, jsonStringSize+1);
jsonString[jsonStringSize++] = ',';
}
// Pop the iterator and object from the stack
sq_pop(vm, 2);
// If there were entries, place the closing ']' one space back to erase the trailing comma
if(outputSizeSnapshot != jsonStringSize)
{
jsonString[jsonStringSize-1] = ']';
}
else
{
// Grow storage to account for ']'
growStorage(jsonString, jsonStringStorageSize, jsonStringSize+1);
jsonString[jsonStringSize++] = ']';
}
return SQ_OK;
}
/// Recursively encodes a Squirrel string into a strict JSON string.
/// \param vm the instance of the VM to use.
/// \param object the Squirrel object to be encoded.
/// \param jsonString the storage for the encoded JSON string (must be first initialied by sq_malloc and ultimately freed with sq_free by the user).
/// \param jsonStringStorageSize the size of the encoded JSON string storage.
/// \param jsonStringSize the size of the writen/decoded JSON string so far.
/// \param depth the current recursion depth (leave blank on first call).
/// \returns SQ_OK or SQ_ERROR if the Squirrel string could not be encoded.
/// \throws an error message if the Squirrel string could not be encoded.
SQInteger JSON::encodeString(HSQUIRRELVM vm, HSQOBJECT object, SQChar *&jsonString, SQInteger &jsonStringStorageSize, SQInteger &jsonStringSize, SQInteger depth)
{
// Push the string to the stack
sq_pushobject(vm, object);
// Retrieve the string and string size from the stack
const SQChar* string;
SQInteger stringSize;
sq_getstringandsize(vm, -1, &string, &stringSize); // Ignore the return value, we know there's a string here
// Pop the object from the stack
sq_poptop(vm);
// Escape and encode the string
if(SQ_FAILED(escapeAndEncode(vm, string, stringSize, jsonString, jsonStringStorageSize, jsonStringSize)))
{
return SQ_ERROR;
}
return SQ_OK;
}
/// Recursively encodes a Squirrel integer|float into a strict JSON string.
/// \param vm the instance of the VM to use.
/// \param object the Squirrel object to be encoded.
/// \param jsonString the storage for the encoded JSON string (must be first initialied by sq_malloc and ultimately freed with sq_free by the user).
/// \param jsonStringStorageSize the size of the encoded JSON string storage.
/// \param jsonStringSize the size of the writen/decoded JSON string so far.
/// \param depth the current recursion depth (leave blank on first call).
/// \returns SQ_OK or SQ_ERROR if the Squirrel integer|float could not be encoded.
/// \throws an error message if the Squirrel integer|float could not be encoded.
SQInteger JSON::encodeNumber(HSQUIRRELVM vm, HSQOBJECT object, SQChar *&jsonString, SQInteger &jsonStringStorageSize, SQInteger &jsonStringSize, SQInteger depth)
{
// Push the integer|float to the stack and attempt to convert it to a string (which is pushed also)
sq_pushobject(vm, object);
sq_tostring(vm,-1); // Ignore the return value, it won't fail for a known integer|float
// Retrieve the number converted string and string size from the stack
const SQChar* string;
SQInteger stringSize;
sq_getstringandsize(vm, -1, &string, &stringSize); // Ignore the return value, we know there's a string here
// Pop the converted number string and object from the stack
sq_pop(vm, 2);
// Grow storage to account for 'number'
growStorage(jsonString, jsonStringStorageSize, jsonStringSize+stringSize);
// Encode the converted number
memcpy(&jsonString[jsonStringSize], string, stringSize);
jsonStringSize += stringSize;
return SQ_OK;
}
/// Recursively encodes a Squirrel bool into a strict JSON string.
/// \param vm the instance of the VM to use.
/// \param object the Squirrel object to be encoded.
/// \param jsonString the storage for the encoded JSON string (must be first initialied by sq_malloc and ultimately freed with sq_free by the user).
/// \param jsonStringStorageSize the size of the encoded JSON string storage.
/// \param jsonStringSize the size of the writen/decoded JSON string so far.
/// \param depth the current recursion depth (leave blank on first call).
/// \returns SQ_OK or SQ_ERROR if the Squirrel bool could not be encoded.
/// \throws an error message if the Squirrel bool could not be encoded.
SQInteger JSON::encodeBool(HSQUIRRELVM vm, HSQOBJECT object, SQChar *&jsonString, SQInteger &jsonStringStorageSize, SQInteger &jsonStringSize, SQInteger depth)
{
// Determine if the bool is true or false
if(object._unVal.nInteger)
{
// Grow storage to account for 'true'
growStorage(jsonString, jsonStringStorageSize, jsonStringSize+4);
// Encode the true bool
jsonString[jsonStringSize] = 't';
jsonString[jsonStringSize+1] = 'r';
jsonString[jsonStringSize+2] = 'u';
jsonString[jsonStringSize+3] = 'e';
jsonStringSize += 4;
}
else
{
// Grow storage to account for 'false'
growStorage(jsonString, jsonStringStorageSize, jsonStringSize+5);
// Encode the false bool
jsonString[jsonStringSize] = 'f';
jsonString[jsonStringSize+1] = 'a';
jsonString[jsonStringSize+2] = 'l';
jsonString[jsonStringSize+3] = 's';
jsonString[jsonStringSize+4] = 'e';
jsonStringSize += 5;
}
return SQ_OK;
}
/// Recursively encodes a Squirrel null into a strict JSON string.
/// \param vm the instance of the VM to use.
/// \param object the Squirrel object to be encoded.
/// \param jsonString the storage for the encoded JSON string (must be first initialied by sq_malloc and ultimately freed with sq_free by the user).
/// \param jsonStringStorageSize the size of the encoded JSON string storage.
/// \param jsonStringSize the size of the writen/decoded JSON string so far.
/// \param depth the current recursion depth (leave blank on first call).
/// \returns SQ_OK or SQ_ERROR if the Squirrel null could not be encoded.
/// \throws an error message if the Squirrel null could not be encoded.
SQInteger JSON::encodeNull(HSQUIRRELVM vm, HSQOBJECT object, SQChar *&jsonString, SQInteger &jsonStringStorageSize, SQInteger &jsonStringSize, SQInteger depth)
{
// Grow storage to account for 'null'
growStorage(jsonString, jsonStringStorageSize, jsonStringSize+4);
// Encode the null
jsonString[jsonStringSize] = 'n';
jsonString[jsonStringSize+1] = 'u';
jsonString[jsonStringSize+2] = 'l';
jsonString[jsonStringSize+3] = 'l';
jsonStringSize += 4;
return SQ_OK;
}
/// Recursively encodes a Squirrel instance (including blobs) into a strict JSON string.
/// \param vm the instance of the VM to use.
/// \param object the Squirrel object to be encoded.
/// \param jsonString the storage for the encoded JSON string (must be first initialied by sq_malloc and ultimately freed with sq_free by the user).
/// \param jsonStringStorageSize the size of the encoded JSON string storage.
/// \param jsonStringSize the size of the writen/decoded JSON string so far.
/// \param depth the current recursion depth (leave blank on first call).
/// \returns SQ_OK or SQ_ERROR if the Squirrel instance could not be encoded.
/// \throws an error message if the Squirrel instance could not be encoded.
SQInteger JSON::encodeInstance(HSQUIRRELVM vm, HSQOBJECT object, SQChar *&jsonString, SQInteger &jsonStringStorageSize, SQInteger &jsonStringSize, SQInteger depth)
{
// Push the instance to the stack and retrieve its typetag (that identifies if it's a special type)
SQUserPointer typeTag;
sq_pushobject(vm, object);
sq_gettypetag(vm, -1, &typeTag);
// Determine if the instance if of type 'blob', otherwise treat it generically
if((SQUnsignedInteger)typeTag == 0x80000002)
{
// The instance was a blob, retrieve a reference to its inner data structure and size
const SQChar* blobData;
sqstd_getblob(vm, -1, (SQUserPointer*)&blobData);
SQInteger blobDataSize = sqstd_getblobsize(vm, -1);
// Pop the blob object from the stack
sq_poptop(vm);
// Escape and encode the blob
if(SQ_FAILED(escapeAndEncode(vm, blobData, blobDataSize, jsonString, jsonStringStorageSize, jsonStringSize)))
{
return SQ_ERROR;
}
}
else
{
// Determine if the instance has a '_serializeRaw' function (will pop the key and push result to the stack)
sq_pushstringex(vm, "_serializeRaw", -1, SQTrue);
if(SQ_SUCCEEDED(sq_get(vm, -2)) && sq_gettype(vm, -1) == OT_CLOSURE)
{
// Push the instance object to the stack as the 'this' parameter and call '_serializeRaw'
sq_pushobject(vm, object);
if(SQ_FAILED(sq_call(vm,1,true,true)))
{
return sq_throwerror(vm, "Unable to execute instance's _serializeRaw() function");
}
// Attempt to convert the returned value to a string, pushing the result to the stack
if(SQ_FAILED(sq_tostring(vm, -1)))
{
return sq_throwerror(vm, "Instance's _serializeRaw did not produce a tostring(able) output");
}
// Retrieve the converted string and string size from the stack
const SQChar* string;
SQInteger stringSize;
sq_getstringandsize(vm, -1, &string, &stringSize); // Ignore the return value, we know there's a string here
// Grow storage to account for '"string"'
growStorage(jsonString, jsonStringStorageSize, jsonStringSize+2+stringSize);
jsonString[jsonStringSize++] = '"';
memcpy(&jsonString[jsonStringSize], string, stringSize);
jsonStringSize += stringSize;
jsonString[jsonStringSize++] = '"';
// Pop the instance object, function closure and converted string from the stack
// (we're doing this here as the lifetime of the converted string is questionable)
sq_pop(vm, 3);
return SQ_OK;
}
// Determine if the instance has a '_serialize' function (will pop the key and push result to the stack)
sq_pushstringex(vm, "_serialize", -1, SQTrue);
if(SQ_SUCCEEDED(sq_get(vm, -2)) && sq_gettype(vm, -1) == OT_CLOSURE)
{
// Push the instance object to the stack as the 'this' parameter and call '_serialize'
sq_pushobject(vm, object);
if(SQ_FAILED(sq_call(vm,1,true,true)))
{
return sq_throwerror(vm, "Unable to execute instance's _serialize() function");
}
// Retrieve the returned object from the stack and recursively encode it
HSQOBJECT returnedObject;
sq_resetobject(&returnedObject);
sq_getstackobj(vm, -1, &returnedObject);
if(SQ_FAILED(encodeObject(vm, returnedObject, jsonString, jsonStringStorageSize, jsonStringSize, depth+1)))
{
return SQ_ERROR;
}
// Pop the instance object, function closure and returned object from the stack
// (we're doing this here as the lifetime of the returned object is questionable)
sq_pop(vm, 3);
return SQ_OK;
}
// Determine if the instance is nexti iteratable by trying and seeing if an error was produced
sq_reseterror(vm);
// Grow storage to account for '{'
growStorage(jsonString, jsonStringStorageSize, jsonStringSize+1);
jsonString[jsonStringSize++] = '{';
// Take a snapshot of the jsonStringSize to revert should nexti fail
SQInteger outputSizeSnapshot = jsonStringSize;
// Push a blank iterator to the stack
sq_pushnull(vm);
// Iterate over each entry within the instance via nexti (will push key and value onto the stack)
while(SQ_SUCCEEDED(sq_next(vm, -2)))
{
// Retrieve the value
HSQOBJECT value;
sq_resetobject(&value);
sq_getstackobj(vm, -1, &value);
// Ignore the entry if it refers to a function (we're only serializing properties)
if(!sq_isfunction(value))
{
// Attempt to convert the key to a string (nexti doesn't guarantee strings)
if(SQ_FAILED(sq_tostring(vm, -2)))
{
return sq_throwerror(vm, "Instance nexti key could not be converted into a string");
}
// Retrieve the converted string and string size from the stack
const SQChar* key;
SQInteger keySize;
sq_getstringandsize(vm, -1, &key, &keySize); // Ignore the return value, we know there's a string here
// Grow storage to account for '"key":'
growStorage(jsonString, jsonStringStorageSize, jsonStringSize+3+keySize);
// Encode the key
jsonString[jsonStringSize++] = '"';
memcpy(&jsonString[jsonStringSize], key, keySize);
jsonStringSize += keySize;
jsonString[jsonStringSize++] = '"';
jsonString[jsonStringSize++] = ':';
// Pop both the key, value and converted string from the stack before we recurse to reduce stack depth impact
sq_pop(vm, 3);
// Recurse to encode the value
if(SQ_FAILED(encodeObject(vm, value, jsonString, jsonStringStorageSize, jsonStringSize, depth+1)))
{
return SQ_ERROR;
}
// Grow storage to account for ','
growStorage(jsonString, jsonStringStorageSize, jsonStringSize+1);
jsonString[jsonStringSize++] = ',';
}
else
{
// Pop both the key and value from the stack
sq_pop(vm, 2);
}
}
// Pop the iterator from the stack (we're not done with the instance object yet)
sq_poptop(vm);
// If there were entries, place the closing '}' one space back to erase the trailing comma
if(outputSizeSnapshot != jsonStringSize)
{
jsonString[jsonStringSize-1] = '}';
}
else
{
// Grow storage to account for '}'
growStorage(jsonString, jsonStringStorageSize, jsonStringSize+1);
jsonString[jsonStringSize++] = '}';
}
// Retrieve the last error onto and then from the stack, popping the retrieved value
sq_getlasterror(vm);
HSQOBJECT subObject;
sq_resetobject(&subObject);
sq_getstackobj(vm, -1, &subObject);
sq_poptop(vm);
// If the last error was not null, nexti failed, instead, extract the instance's class
// and iterate over this instead as a fallback
if(!sq_isnull(subObject))
{
// Rewind the jsonStringSize position
jsonStringSize = outputSizeSnapshot - 1;
// Fetch the instance's class onto and from the stack, immediately popping the value and instance object
sq_getclass(vm, -1);
sq_resetobject(&subObject);
sq_getstackobj(vm, -1, &subObject);
sq_pop(vm, 2);
// Recurse to encode the value (same depth as it's a fallback)
if(SQ_FAILED(encodeObject(vm, subObject, jsonString, jsonStringStorageSize, jsonStringSize, depth)))
{
return SQ_ERROR;
}
}
else
{
// Pop the instance object
sq_poptop(vm);
}
}
return SQ_OK;
}
/// Interprets byte data as a string and escapes it in a JSON compliant manner.
/// \param vm the instance of the VM to use.
/// \param data the data to be escaped as a string.
/// \param dataSize the size of the data to be escaped.
/// \param jsonString the storage for the encoded JSON string (must be first initialied by sq_malloc and ultimately freed with sq_free by the user).
/// \param jsonStringStorageSize the size of the encoded JSON string storage.
/// \param jsonStringSize the size of the writen/decoded JSON string so far.
/// \returns SQ_OK or SQ_ERROR if the data could not be escaped.
/// \throws an error message if the data could not be escaped.
SQInteger JSON::escapeAndEncode(HSQUIRRELVM vm, const SQChar *&data, SQInteger dataSize, SQChar *&jsonString, SQInteger &jsonStringStorageSize, SQInteger &jsonStringSize)
{
// Pre-grow storage to account for '"dataAsString"' to reduce number of mallocs
growStorage(jsonString, jsonStringStorageSize, jsonStringSize+2+dataSize);
jsonString[jsonStringSize++] = '"';
for(int i = 0; i < dataSize; ++i)
{
unsigned char character = data[i];
// Determine if the character can be represented as 7-bit ASCII or is unicode
if((character & 0x80) == 0x00)
{
// Character was 7-bit ASCII, search for escape sequences
switch(character)
{
case '"': case '\\': case '/': case '\b': case '\f':
case '\n': case '\r': case '\t': case '\0':
growStorage(jsonString, jsonStringStorageSize, jsonStringSize+2);
jsonString[jsonStringSize++] = '\\';
break;
default:
growStorage(jsonString, jsonStringStorageSize, jsonStringSize+1);
jsonString[jsonStringSize++] = character;
continue;
}
switch(character)
{
case '"': jsonString[jsonStringSize++] = '"'; break;
case '\\': jsonString[jsonStringSize++] = '\\'; break;
case '/': jsonString[jsonStringSize++] = '/'; break;
case '\b': jsonString[jsonStringSize++] = 'b'; break;
case '\f': jsonString[jsonStringSize++] = 'f'; break;
case '\n': jsonString[jsonStringSize++] = 'n'; break;
case '\r': jsonString[jsonStringSize++] = 'r'; break;
case '\t': jsonString[jsonStringSize++] = 't'; break;
case '\0':
growStorage(jsonString, jsonStringStorageSize, jsonStringSize+5);
jsonString[jsonStringSize] = 'u';
jsonString[jsonStringSize+1] = '0';
jsonString[jsonStringSize+2] = '0';
jsonString[jsonStringSize+3] = '0';
jsonString[jsonStringSize+4] = '0';
jsonStringSize += 5;
break;
default: break;
}
}
else
{
// Character was unicode, determine which format and encode
if((character & 0xE0) == 0xC0)
{
// 2-byte unicode
if(i+1 >= dataSize)
{
return sq_throwerror(vm, "Unable to escape data as unicode");
}
growStorage(jsonString, jsonStringStorageSize, jsonStringSize+2);
jsonString[jsonStringSize] = character;
jsonString[jsonStringSize+1] = data[++i];
jsonStringSize += 2;
}
else if((character & 0xF0) == 0xE0)
{
// 3-byte unicode
if(i+2 >= dataSize)
{
return sq_throwerror(vm, "Unable to escape data as unicode");
}
growStorage(jsonString, jsonStringStorageSize, jsonStringSize+3);
jsonString[jsonStringSize] = character;
jsonString[jsonStringSize+1] = data[++i];
jsonString[jsonStringSize+2] = data[++i];
jsonStringSize += 3;
}
else if((character & 0xF8) == 0xF0)
{
// 4-byte unicode
if(i+3 >= dataSize)
{
return sq_throwerror(vm, "Unable to escape data as unicode");
}
growStorage(jsonString, jsonStringStorageSize, jsonStringSize+4);
jsonString[jsonStringSize] = character;
jsonString[jsonStringSize+1] = data[++i];
jsonString[jsonStringSize+2] = data[++i];
jsonString[jsonStringSize+3] = data[++i];
jsonStringSize += 4;
}
}
}
growStorage(jsonString, jsonStringStorageSize, jsonStringSize+1);
jsonString[jsonStringSize++] = '"';
return SQ_OK;
}
// Constructor/Destructor
//-----------------------
JSON::JSON()
{
tokenBuffer = (jsmntok_t*)sq_malloc(JSON_MAX_TOKENS*sizeof(jsmntok_t));
}
JSON::~JSON()
{
sq_free(tokenBuffer, JSON_MAX_TOKENS*sizeof(jsmntok_t));
}