454 lines
14 KiB
SourcePawn
454 lines
14 KiB
SourcePawn
/**
|
|
* 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_decode_helpers_included
|
|
#endinput
|
|
#endif
|
|
#define _json_decode_helpers_included
|
|
|
|
#include <string>
|
|
|
|
/**
|
|
* @section Analysing format of incoming JSON cells.
|
|
*/
|
|
|
|
/**
|
|
* Checks whether the character at the given
|
|
* position in the buffer is whitespace.
|
|
*
|
|
* @param buffer String buffer of data.
|
|
* @param pos Position to check in buffer.
|
|
* @returns True if buffer[pos] is whitespace, false otherwise.
|
|
*/
|
|
stock bool json_is_whitespace(const char[] buffer, int &pos) {
|
|
return buffer[pos] == ' '
|
|
|| buffer[pos] == '\t'
|
|
|| buffer[pos] == '\r'
|
|
|| buffer[pos] == '\n';
|
|
}
|
|
|
|
/**
|
|
* Checks whether the character at the beginning
|
|
* of the buffer is the start of a string.
|
|
*
|
|
* @param buffer String buffer of data.
|
|
* @returns True if buffer[0] is the start of a string, false otherwise.
|
|
*/
|
|
stock bool json_is_string(const char[] buffer) {
|
|
return buffer[0] == '"';
|
|
}
|
|
|
|
/**
|
|
* Checks whether the buffer provided contains an int.
|
|
*
|
|
* @param buffer String buffer of data.
|
|
* @returns True if buffer contains an int, false otherwise.
|
|
*/
|
|
stock bool json_is_int(const char[] buffer) {
|
|
bool starts_with_zero = false;
|
|
bool has_digit_gt_zero = false;
|
|
|
|
int length = strlen(buffer);
|
|
for (int i = 0; i < length; ++i) {
|
|
// allow minus as first character only
|
|
if (i == 0 && buffer[i] == '-') {
|
|
continue;
|
|
}
|
|
|
|
if (IsCharNumeric(buffer[i])) {
|
|
if (buffer[i] == '0') {
|
|
if (starts_with_zero) {
|
|
// detect repeating leading zeros
|
|
return false;
|
|
} else if (!has_digit_gt_zero) {
|
|
starts_with_zero = true;
|
|
}
|
|
} else {
|
|
has_digit_gt_zero = true;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// buffer must start with zero and have no other numerics before decimal
|
|
// OR not start with zero and have other numerics
|
|
return ((starts_with_zero && !has_digit_gt_zero) || (!starts_with_zero && has_digit_gt_zero));
|
|
}
|
|
|
|
/**
|
|
* Checks whether the buffer provided contains a float.
|
|
*
|
|
* @param buffer String buffer of data.
|
|
* @returns True if buffer contains a float, false otherwise.
|
|
*/
|
|
stock bool json_is_float(const char[] buffer) {
|
|
bool starts_with_zero = false;
|
|
bool has_digit_gt_zero = false;
|
|
bool after_decimal = false;
|
|
bool has_digit_after_decimal = false;
|
|
bool after_exponent = false;
|
|
bool has_digit_after_exponent = false;
|
|
|
|
int length = strlen(buffer);
|
|
for (int i = 0; i < length; ++i) {
|
|
// allow minus as first character only
|
|
if (i == 0 && buffer[i] == '-') {
|
|
continue;
|
|
}
|
|
|
|
// if we haven't encountered a decimal or exponent yet
|
|
if (!after_decimal && !after_exponent) {
|
|
if (buffer[i] == '.') {
|
|
// if we encounter a decimal before any digits
|
|
if (!starts_with_zero && !has_digit_gt_zero) {
|
|
return false;
|
|
}
|
|
|
|
after_decimal = true;
|
|
} else if (buffer[i] == 'e' || buffer[i] == 'E') {
|
|
// if we encounter an exponent before any non-zero digits
|
|
if (starts_with_zero && !has_digit_gt_zero) {
|
|
return false;
|
|
}
|
|
|
|
after_exponent = true;
|
|
} else if (IsCharNumeric(buffer[i])) {
|
|
if (buffer[i] == '0') {
|
|
if (starts_with_zero) {
|
|
// detect repeating leading zeros
|
|
return false;
|
|
} else if (!has_digit_gt_zero) {
|
|
starts_with_zero = true;
|
|
}
|
|
} else {
|
|
has_digit_gt_zero = true;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
} else if (after_decimal && !after_exponent) {
|
|
// after decimal has been encountered, allow any numerics
|
|
if (IsCharNumeric(buffer[i])) {
|
|
has_digit_after_decimal = true;
|
|
} else if (buffer[i] == 'e' || buffer[i] == 'E') {
|
|
if (!has_digit_after_decimal) {
|
|
// detect exponents directly after decimal
|
|
return false;
|
|
}
|
|
|
|
after_exponent = true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} else if (after_exponent) {
|
|
if ((buffer[i] == '+' || buffer[i] == '-') && (buffer[i - 1] == 'e' || buffer[i - 1] == 'E')) {
|
|
// allow + or - directly after exponent
|
|
continue;
|
|
} else if (IsCharNumeric(buffer[i])) {
|
|
has_digit_after_exponent = true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (starts_with_zero && has_digit_gt_zero) {
|
|
// if buffer starts with zero, there should be no other digits before the decimal
|
|
return false;
|
|
}
|
|
|
|
// if we have a decimal, there should be digit(s) after it
|
|
if (after_decimal) {
|
|
if (!has_digit_after_decimal) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// if we have an exponent, there should be digit(s) after it
|
|
if (after_exponent) {
|
|
if (!has_digit_after_exponent) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// we should have reached an exponent, decimal or both
|
|
// otherwise, this number can be handled by the int parser
|
|
return after_decimal || after_exponent;
|
|
}
|
|
|
|
/**
|
|
* Checks whether the buffer provided contains a bool.
|
|
*
|
|
* @param buffer String buffer of data.
|
|
* @returns True if buffer contains a bool, false otherwise.
|
|
*/
|
|
stock bool json_is_bool(const char[] buffer) {
|
|
return StrEqual(buffer, "true")
|
|
|| StrEqual(buffer, "false");
|
|
}
|
|
|
|
/**
|
|
* Checks whether the buffer provided contains null.
|
|
*
|
|
* @param buffer String buffer of data.
|
|
* @returns True if buffer contains null, false otherwise.
|
|
*/
|
|
stock bool json_is_null(const char[] buffer) {
|
|
return StrEqual(buffer, "null");
|
|
}
|
|
|
|
/**
|
|
* Checks whether the character at the beginning
|
|
* of the buffer is the start of an object.
|
|
*
|
|
* @param buffer String buffer of data.
|
|
* @returns True if buffer[0] is the start of an object, false otherwise.
|
|
*/
|
|
stock bool json_is_object(const char[] buffer) {
|
|
return buffer[0] == '{';
|
|
}
|
|
|
|
/**
|
|
* Checks whether the character at the beginning
|
|
* of the buffer is the end of an object.
|
|
*
|
|
* @param buffer String buffer of data.
|
|
* @returns True if buffer[0] is the end of an object, false otherwise.
|
|
*/
|
|
stock bool json_is_object_end(const char[] buffer) {
|
|
return buffer[0] == '}';
|
|
}
|
|
|
|
/**
|
|
* Checks whether the character at the beginning
|
|
* of the buffer is the start of an array.
|
|
*
|
|
* @param buffer String buffer of data.
|
|
* @returns True if buffer[0] is the start of an array, false otherwise.
|
|
*/
|
|
stock bool json_is_array(const char[] buffer) {
|
|
return buffer[0] == '[';
|
|
}
|
|
|
|
/**
|
|
* Checks whether the character at the beginning
|
|
* of the buffer is the start of an array.
|
|
*
|
|
* @param buffer String buffer of data.
|
|
* @returns True if buffer[0] is the start of an array, false otherwise.
|
|
*/
|
|
stock bool json_is_array_end(const char[] buffer) {
|
|
return buffer[0] == ']';
|
|
}
|
|
|
|
/**
|
|
* Checks whether the character at the given position in the buffer
|
|
* is considered a valid 'end point' for some data, such as a
|
|
* colon (indicating a key), a comma (indicating a new element),
|
|
* or the end of an object or array.
|
|
*
|
|
* @param buffer String buffer of data.
|
|
* @param pos Position to check in buffer.
|
|
* @returns True if buffer[pos] is a valid data end point, false otherwise.
|
|
*/
|
|
stock bool json_is_at_end(const char[] buffer, int &pos, bool is_array) {
|
|
return buffer[pos] == ','
|
|
|| (!is_array && buffer[pos] == ':')
|
|
|| json_is_object_end(buffer[pos])
|
|
|| json_is_array_end(buffer[pos]);
|
|
}
|
|
|
|
/**
|
|
* Moves the position until it reaches a non-whitespace
|
|
* character or the end of the buffer's maximum size.
|
|
*
|
|
* @param buffer String buffer of data.
|
|
* @param maxlen Maximum size of string buffer.
|
|
* @param pos Position to increment.
|
|
* @returns True if pos is not at the end of the buffer, false otherwise.
|
|
*/
|
|
stock bool json_skip_whitespace(const char[] buffer, int maxlen, int &pos) {
|
|
while (json_is_whitespace(buffer, pos) && pos < maxlen) {
|
|
++pos;
|
|
}
|
|
|
|
return pos < maxlen;
|
|
}
|
|
|
|
/**
|
|
* Extracts a JSON cell from the buffer until
|
|
* a valid end point is reached.
|
|
*
|
|
* @param buffer String buffer of data.
|
|
* @param maxlen Maximum size of string buffer.
|
|
* @param pos Position to increment.
|
|
* @param output String buffer to store output.
|
|
* @param output_maxlen Maximum size of output string buffer.
|
|
* @param is_array Whether the decoder is currently processing an array.
|
|
* @returns True if pos is not at the end of the buffer, false otherwise.
|
|
*/
|
|
stock bool json_extract_until_end(const char[] buffer, int maxlen, int &pos, char[] output, int output_maxlen, bool is_array) {
|
|
strcopy(output, output_maxlen, "");
|
|
|
|
int start = pos;
|
|
// while we haven't reached whitespace, a valid end point or the maxlen, increment position
|
|
while (!json_is_whitespace(buffer, pos) && !json_is_at_end(buffer, pos, is_array) && pos < maxlen) {
|
|
++pos;
|
|
}
|
|
int end = pos;
|
|
|
|
// skip any following whitespace
|
|
json_skip_whitespace(buffer, maxlen, pos);
|
|
|
|
// if we aren't at a valid endpoint, extraction failed
|
|
if (!json_is_at_end(buffer, pos, is_array)) {
|
|
return false;
|
|
}
|
|
|
|
// copy only from start with length end - start + NULL
|
|
strcopy(output, end - start + 1, buffer[start]);
|
|
|
|
return pos < maxlen;
|
|
}
|
|
|
|
|
|
/**
|
|
* Extracts a JSON string from the buffer until
|
|
* a valid end point is reached.
|
|
*
|
|
* @param buffer String buffer of data.
|
|
* @param maxlen Maximum size of string buffer.
|
|
* @param pos Position to increment.
|
|
* @param output String buffer to store output.
|
|
* @param output_maxlen Maximum size of output string buffer.
|
|
* @param is_array Whether the decoder is currently processing an array.
|
|
* @returns True if pos is not at the end of the buffer, false otherwise.
|
|
*/
|
|
stock bool json_extract_string(const char[] buffer, int maxlen, int &pos, char[] output, int output_maxlen, bool is_array) {
|
|
// extracts a string which needs to be quote-escaped
|
|
strcopy(output, output_maxlen, "");
|
|
|
|
// increment past opening quote
|
|
++pos;
|
|
|
|
// set start to position of first character in string
|
|
int start = pos;
|
|
|
|
// while we haven't hit the end of the buffer
|
|
while (pos < maxlen) {
|
|
// check for unescaped control characters
|
|
if (buffer[pos] == '\b'
|
|
|| buffer[pos] == '\f'
|
|
|| buffer[pos] == '\n'
|
|
|| buffer[pos] == '\r'
|
|
|| buffer[pos] == '\t') {
|
|
return false;
|
|
}
|
|
|
|
// pass over any non-quote character
|
|
if (buffer[pos] != '"') {
|
|
++pos;
|
|
continue;
|
|
}
|
|
|
|
// count preceding backslashes to check if quote is escaped
|
|
int search_pos = pos;
|
|
int preceding_backslashes = 0;
|
|
while (search_pos > 0 && buffer[--search_pos] == '\\') {
|
|
++preceding_backslashes;
|
|
}
|
|
|
|
// if we have an even number of backslashes, the quote is not escaped
|
|
if (preceding_backslashes % 2 == 0) {
|
|
break;
|
|
}
|
|
|
|
// otherwise, pass over the quote as it must be escaped
|
|
++pos;
|
|
}
|
|
|
|
// set end to the current position
|
|
int end = pos;
|
|
|
|
// jump 1 ahead since we ended on " instead of an ending char
|
|
++pos;
|
|
|
|
// skip trailing whitespace
|
|
if (!json_skip_whitespace(buffer, maxlen, pos)) {
|
|
return false;
|
|
}
|
|
|
|
// if we haven't reached an ending character at the end of the cell
|
|
// there is likely junk data not encapsulated by a string
|
|
if (!json_is_at_end(buffer, pos, is_array)) {
|
|
return false;
|
|
}
|
|
|
|
// copy only from start with length end - start + NULL
|
|
strcopy(output, end - start + 1, buffer[start]);
|
|
json_unescape_string(output, maxlen);
|
|
|
|
return pos < maxlen;
|
|
}
|
|
|
|
/**
|
|
* Extracts an int from the buffer.
|
|
*
|
|
* @param buffer String buffer of data.
|
|
* @returns Int value of the buffer.
|
|
*/
|
|
stock int json_extract_int(const char[] buffer) {
|
|
return StringToInt(buffer);
|
|
}
|
|
|
|
/**
|
|
* Extracts a float from the buffer.
|
|
*
|
|
* @param buffer String buffer of data.
|
|
* @returns Float value of the buffer.
|
|
*/
|
|
stock float json_extract_float(const char[] buffer) {
|
|
return StringToFloat(buffer);
|
|
}
|
|
|
|
/**
|
|
* Extracts a bool from the buffer.
|
|
*
|
|
* @param buffer String buffer of data.
|
|
* @returns Bool value of the buffer.
|
|
*/
|
|
stock bool json_extract_bool(const char[] buffer) {
|
|
return StrEqual(buffer, "true");
|
|
}
|