431 lines
14 KiB
PHP
431 lines
14 KiB
PHP
|
/**
|
||
|
* vim: set ts=4 :
|
||
|
* =============================================================================
|
||
|
* sm-json
|
||
|
* Provides a pure SourcePawn implementation of JSON encoding and decoding.
|
||
|
* https://github.com/clugg/sm-json
|
||
|
*
|
||
|
* sm-json (C)2018 James Dickens. (clug)
|
||
|
* SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved.
|
||
|
* =============================================================================
|
||
|
*
|
||
|
* This program is free software; you can redistribute it and/or modify it under
|
||
|
* the terms of the GNU General Public License, version 3.0, as published by the
|
||
|
* Free Software Foundation.
|
||
|
*
|
||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||
|
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||
|
* details.
|
||
|
*
|
||
|
* You should have received a copy of the GNU General Public License along with
|
||
|
* this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
*
|
||
|
* As a special exception, AlliedModders LLC gives you permission to link the
|
||
|
* code of this program (as well as its derivative works) to "Half-Life 2," the
|
||
|
* "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software
|
||
|
* by the Valve Corporation. You must obey the GNU General Public License in
|
||
|
* all respects for all other code used. Additionally, AlliedModders LLC grants
|
||
|
* this exception to all derivative works. AlliedModders LLC defines further
|
||
|
* exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
|
||
|
* or <http://www.sourcemod.net/license.php>.
|
||
|
*/
|
||
|
|
||
|
#if defined _json_included
|
||
|
#endinput
|
||
|
#endif
|
||
|
#define _json_included
|
||
|
|
||
|
#include <string>
|
||
|
#include <json/definitions>
|
||
|
#include <json/string_helpers>
|
||
|
#include <json/encode_helpers>
|
||
|
#include <json/decode_helpers>
|
||
|
#include <json/object>
|
||
|
|
||
|
/**
|
||
|
* Encodes a JSON_Object into its string representation.
|
||
|
*
|
||
|
* @param obj Object to encode.
|
||
|
* @param output String buffer to store output.
|
||
|
* @param maxlen Maximum size of string buffer.
|
||
|
* @param pretty_print Should the output be pretty printed (newlines and spaces)? [optional, default: false]
|
||
|
* @param depth The current depth of the encoder. [optional, default: 0]
|
||
|
*/
|
||
|
stock void json_encode(JSON_Object obj, char[] output, int maxlen, bool pretty_print = false, int depth = 0) {
|
||
|
bool is_array = obj.IsArray;
|
||
|
bool is_empty = true;
|
||
|
int builder_size;
|
||
|
|
||
|
// used in key iterator
|
||
|
int str_length = 1;
|
||
|
int int_value;
|
||
|
int cell_length = 0;
|
||
|
|
||
|
strcopy(output, maxlen, (is_array) ? "[" : "{");
|
||
|
|
||
|
StringMapSnapshot snap = obj.Snapshot();
|
||
|
int key_length = 0;
|
||
|
for (int i = 0; i < obj.Length; ++i) {
|
||
|
key_length = snap.KeyBufferSize(i);
|
||
|
char[] key = new char[key_length];
|
||
|
|
||
|
if (is_array) {
|
||
|
obj.GetIndexString(key, key_length, i);
|
||
|
} else {
|
||
|
snap.GetKey(i, key, key_length);
|
||
|
}
|
||
|
|
||
|
// skip meta-keys
|
||
|
if (json_is_meta_key(key)) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// skip keys that are marked as hidden
|
||
|
if (obj.GetKeyHidden(key)) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// skip keys of unknown type
|
||
|
JSON_CELL_TYPE type = obj.GetKeyType(key);
|
||
|
if (type == Type_Invalid) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// if we are dealing with a string, prepare the str_value variable for fetching
|
||
|
if (type == Type_String) {
|
||
|
str_length = obj.GetKeyLength(key);
|
||
|
}
|
||
|
char[] str_value = new char[str_length + 1];
|
||
|
|
||
|
// determine the length of the char[] needed to represent our cell data
|
||
|
cell_length = 0;
|
||
|
switch (type) {
|
||
|
case Type_String: {
|
||
|
// get the string value early, as its cell_length is determined by its contents
|
||
|
obj.GetString(key, str_value, str_length + 1);
|
||
|
cell_length = json_cell_string_size(str_length);
|
||
|
}
|
||
|
case Type_Int: {
|
||
|
// get the int value early, as its cell_length is determined by its contents
|
||
|
int_value = obj.GetInt(key);
|
||
|
cell_length = json_cell_int_size(int_value);
|
||
|
}
|
||
|
case Type_Float: {
|
||
|
cell_length = json_cell_float_size();
|
||
|
}
|
||
|
case Type_Bool: {
|
||
|
cell_length = json_cell_bool_size();
|
||
|
}
|
||
|
case Type_Null: {
|
||
|
cell_length = json_cell_null_size();
|
||
|
}
|
||
|
case Type_Object: {
|
||
|
cell_length = maxlen;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// fit the contents into the cell
|
||
|
char[] cell = new char[cell_length];
|
||
|
switch (type) {
|
||
|
case Type_String: {
|
||
|
json_cell_string(str_value, cell, cell_length);
|
||
|
}
|
||
|
case Type_Int: {
|
||
|
json_cell_int(int_value, cell, cell_length);
|
||
|
}
|
||
|
case Type_Float: {
|
||
|
float value = obj.GetFloat(key);
|
||
|
json_cell_float(value, cell, cell_length);
|
||
|
}
|
||
|
case Type_Bool: {
|
||
|
bool value = obj.GetBool(key);
|
||
|
json_cell_bool(value, cell, cell_length);
|
||
|
}
|
||
|
case Type_Null: {
|
||
|
json_cell_null(cell, cell_length);
|
||
|
}
|
||
|
case Type_Object: {
|
||
|
JSON_Object value = obj.GetObject(key);
|
||
|
json_encode(value, cell, cell_length, pretty_print, depth + 1);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// make the builder fit our key:value
|
||
|
builder_size = cell_length + 1; // use previously determined cell length and +1 for comma
|
||
|
if (!is_array) {
|
||
|
builder_size += json_cell_string_size(strlen(key)) + 1; // get the length of the key, +1 for :
|
||
|
}
|
||
|
|
||
|
if (pretty_print && !is_array) {
|
||
|
// 1 space after colon
|
||
|
builder_size += strlen(JSON_PP_AFTER_COLON);
|
||
|
}
|
||
|
|
||
|
char[] builder = new char[builder_size];
|
||
|
strcopy(builder, builder_size, "");
|
||
|
// add the key if it's an object
|
||
|
if (!is_array) {
|
||
|
json_cell_string(key, builder, builder_size);
|
||
|
StrCat(builder, builder_size, ":");
|
||
|
if (pretty_print) {
|
||
|
StrCat(builder, builder_size, JSON_PP_AFTER_COLON);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// add the value and a trailing comma
|
||
|
StrCat(builder, builder_size, cell);
|
||
|
StrCat(builder, builder_size, ",");
|
||
|
|
||
|
if (pretty_print) {
|
||
|
StrCat(output, maxlen, JSON_PP_NEWLINE);
|
||
|
for (int j = 0; j < depth + 1; ++j) {
|
||
|
StrCat(output, maxlen, JSON_PP_INDENT);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// send builder to output
|
||
|
StrCat(output, maxlen, builder);
|
||
|
|
||
|
is_empty = false;
|
||
|
}
|
||
|
|
||
|
delete snap;
|
||
|
|
||
|
if (!is_empty) {
|
||
|
// remove the final comma
|
||
|
output[strlen(output) - 1] = '\0';
|
||
|
}
|
||
|
|
||
|
if (pretty_print) {
|
||
|
StrCat(output, maxlen, JSON_PP_NEWLINE);
|
||
|
}
|
||
|
|
||
|
if (pretty_print) {
|
||
|
for (int j = 0; j < depth; ++j) {
|
||
|
StrCat(output, maxlen, JSON_PP_INDENT);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// append closing bracket
|
||
|
StrCat(output, maxlen, (is_array) ? "]" : "}");
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Decodes a JSON string into its JSON_Object representation.
|
||
|
*
|
||
|
* @param buffer Buffer to decode.
|
||
|
* @param result Object to store output in. Setting this allows loading over an existing JSON_Object, 'refreshing' it as opposed to creating a new one. [optional, default: null]
|
||
|
* @param pos Current position of the decoder as a bytes offset into the buffer.
|
||
|
* @param depth Current depth of the decoder as child elements in the object.
|
||
|
* @returns JSON_Object representation of buffer, or null if decoding failed (buffer didn't contain valid JSON).
|
||
|
*/
|
||
|
stock JSON_Object json_decode(const char[] buffer, JSON_Object result = null, int &pos = 0, int depth = 0) {
|
||
|
int length = strlen(buffer);
|
||
|
bool is_array = false;
|
||
|
|
||
|
// skip preceding whitespace
|
||
|
if (!json_skip_whitespace(buffer, length, pos)) {
|
||
|
//LogError("json_decode: buffer ended early at %d", pos);
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
if (json_is_object(buffer[pos])) {
|
||
|
is_array = false;
|
||
|
} else if (json_is_array(buffer[pos])) {
|
||
|
is_array = true;
|
||
|
} else {
|
||
|
//LogError("json_decode: character not identified as object or array at %d", pos);
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
if (result == null) {
|
||
|
result = new JSON_Object(is_array);
|
||
|
}
|
||
|
|
||
|
bool empty_checked = false;
|
||
|
char[] key = new char[length];
|
||
|
char[] cell = new char[length];
|
||
|
|
||
|
// while we haven't reached the end of our structure
|
||
|
while ((!is_array && !json_is_object_end(buffer[pos]))
|
||
|
|| (is_array && !json_is_array_end(buffer[pos]))) {
|
||
|
// pos is either an opening structure or comma, so increment past it
|
||
|
++pos;
|
||
|
|
||
|
// if we are at the end of an object or array
|
||
|
// and haven't checked for empty yet, we can stop here (empty structure)
|
||
|
if (((!is_array && json_is_object_end(buffer[pos]))
|
||
|
|| (is_array && json_is_array_end(buffer[pos])))
|
||
|
&& !empty_checked) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
empty_checked = true;
|
||
|
|
||
|
// skip any whitespace preceding the element
|
||
|
if (!json_skip_whitespace(buffer, length, pos)) {
|
||
|
//LogError("json_decode: buffer ended early at %d", pos);
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
// if dealing with an object, look for the key
|
||
|
if (!is_array) {
|
||
|
if (!json_is_string(buffer[pos])) {
|
||
|
//LogError("json_decode: expected key string at %d", pos);
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
// extract the key from the buffer
|
||
|
json_extract_string(buffer, length, pos, key, length, is_array);
|
||
|
|
||
|
// skip any whitespace following the key
|
||
|
if (!json_skip_whitespace(buffer, length, pos)) {
|
||
|
//LogError("json_decode: buffer ended early at %d", pos);
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
// ensure that we find a colon
|
||
|
if (buffer[pos++] != ':') {
|
||
|
//LogError("json_decode: expected colon after key at %d", pos);
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
// skip any whitespace following the colon
|
||
|
if (!json_skip_whitespace(buffer, length, pos)) {
|
||
|
//LogError("json_decode: buffer ended early at %d", pos);
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (json_is_object(buffer[pos]) || json_is_array(buffer[pos])) {
|
||
|
// if we are dealing with an object or array
|
||
|
// fetch the existing object if one exists at the key
|
||
|
JSON_Object current = (!is_array) ? result.GetObject(key) : null;
|
||
|
|
||
|
// decode recursively
|
||
|
JSON_Object value = json_decode(buffer, current, pos, depth + 1);
|
||
|
|
||
|
// decoding failed, error will be logged in json_decode
|
||
|
if (value == null) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
if (is_array) {
|
||
|
result.PushObject(value);
|
||
|
} else {
|
||
|
result.SetObject(key, value);
|
||
|
}
|
||
|
} else if (json_is_string(buffer[pos])) {
|
||
|
// if we are dealing with a string, attempt to extract it
|
||
|
if (!json_extract_string(buffer, length, pos, cell, length, is_array)) {
|
||
|
//LogError("json_decode: couldn't extract string at %d", pos);
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
if (is_array) {
|
||
|
result.PushString(cell);
|
||
|
} else {
|
||
|
result.SetString(key, cell);
|
||
|
}
|
||
|
} else {
|
||
|
if (!json_extract_until_end(buffer, length, pos, cell, length, is_array)) {
|
||
|
//LogError("json_decode: couldn't extract until end at %d", pos);
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
if (strlen(cell) == 0) {
|
||
|
//LogError("json_decode: empty cell encountered at %d", pos);
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
if (json_is_int(cell)) {
|
||
|
int value = json_extract_int(cell);
|
||
|
if (is_array) {
|
||
|
result.PushInt(value);
|
||
|
} else {
|
||
|
result.SetInt(key, value);
|
||
|
}
|
||
|
} else if (json_is_float(cell)) {
|
||
|
float value = json_extract_float(cell);
|
||
|
if (is_array) {
|
||
|
result.PushFloat(value);
|
||
|
} else {
|
||
|
result.SetFloat(key, value);
|
||
|
}
|
||
|
} else if (json_is_bool(cell)) {
|
||
|
bool value = json_extract_bool(cell);
|
||
|
if (is_array) {
|
||
|
result.PushBool(value);
|
||
|
} else {
|
||
|
result.SetBool(key, value);
|
||
|
}
|
||
|
} else if (json_is_null(cell)) {
|
||
|
if (is_array) {
|
||
|
result.PushHandle(null);
|
||
|
} else {
|
||
|
result.SetHandle(key, null);
|
||
|
}
|
||
|
} else {
|
||
|
//LogError("json_decode: unknown type encountered at %d: %s", pos, cell);
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!json_skip_whitespace(buffer, length, pos)) {
|
||
|
//LogError("json_decode: buffer ended early at %d", pos);
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// skip remaining whitespace and ensure we're at the end of the buffer
|
||
|
++pos;
|
||
|
if (json_skip_whitespace(buffer, length, pos) && depth == 0) {
|
||
|
//LogError("json_decode: unexpected data after end of structure at %d", pos);
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Recursively cleans up JSON_Objects and any objects referenced within.
|
||
|
*
|
||
|
* @param obj JSON_Object to clean up.
|
||
|
*/
|
||
|
void json_cleanup(JSON_Object obj) {
|
||
|
bool is_array = obj.IsArray;
|
||
|
|
||
|
int key_length = 0;
|
||
|
StringMapSnapshot snap = obj.Snapshot();
|
||
|
for (int i = 0; i < snap.Length; ++i) {
|
||
|
key_length = snap.KeyBufferSize(i);
|
||
|
char[] key = new char[key_length];
|
||
|
|
||
|
// ignore meta keys
|
||
|
snap.GetKey(i, key, key_length);
|
||
|
if (json_is_meta_key(key)) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// only clean up objects
|
||
|
JSON_CELL_TYPE type = obj.GetKeyType(key);
|
||
|
if (type != Type_Object) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
JSON_Object nested_obj = obj.GetObject(key);
|
||
|
if (nested_obj != null) {
|
||
|
nested_obj.Cleanup();
|
||
|
delete nested_obj;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
obj.Clear();
|
||
|
delete snap;
|
||
|
|
||
|
if (is_array) {
|
||
|
obj.SetValue(JSON_ARRAY_INDEX_KEY, 0);
|
||
|
}
|
||
|
}
|