Skip to content

Commit 35749cb

Browse files
committed
JSON parser improvements:
- Support the first element of an array being null, while still supporting empty arrays as well. - Fix empty object members so our (compact) output is always valid JSON. - Fix JSON with "unset" values in an array not resulting in valid output in toString - Added JSON unit tests
1 parent 5749978 commit 35749cb

4 files changed

Lines changed: 62 additions & 36 deletions

File tree

lib/json.cpp

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ JSON::Iter::Iter(Value &root){
1515
myType = root.myType;
1616
i = 0;
1717
r = &root;
18-
if (!root.size()){myType = JSON::EMPTY;}
18+
if (!root.size()) { myType = JSON::UNSET; }
1919
if (myType == JSON::ARRAY){aIt = root.arrVal.begin();}
2020
if (myType == JSON::OBJECT){oIt = root.objVal.begin();}
2121
}
@@ -86,7 +86,7 @@ JSON::ConstIter::ConstIter(const Value &root){
8686
myType = root.myType;
8787
i = 0;
8888
r = &root;
89-
if (!root.size()){myType = JSON::EMPTY;}
89+
if (!root.size()) { myType = JSON::UNSET; }
9090
if (myType == JSON::ARRAY){aIt = root.arrVal.begin();}
9191
if (myType == JSON::OBJECT){oIt = root.objVal.begin();}
9292
}
@@ -333,9 +333,9 @@ static void skipToEnd(std::istream &fromstream){
333333
}
334334
}
335335

336-
/// Sets this JSON::Value to null;
336+
/// Sets this JSON::Value to "unset"
337337
JSON::Value::Value(){
338-
null();
338+
unset();
339339
}
340340

341341
/// Sets this JSON::Value to null
@@ -350,7 +350,7 @@ JSON::Value::Value(const Value &rhs){
350350

351351
/// Sets this JSON::Value to read from this position in the std::istream
352352
JSON::Value::Value(std::istream &fromstream){
353-
null();
353+
unset();
354354
bool reading_object = false;
355355
bool reading_array = false;
356356
bool negative = false;
@@ -368,7 +368,7 @@ JSON::Value::Value(std::istream &fromstream){
368368
c = fromstream.get();
369369
myType = ARRAY;
370370
Value tmp = JSON::Value(fromstream);
371-
if (tmp.myType != EMPTY){append(tmp);}
371+
if (tmp.myType != UNSET) { append(tmp); }
372372
break;
373373
}
374374
case '\'':
@@ -528,7 +528,7 @@ bool JSON::Value::operator==(const JSON::Value &rhs) const{
528528
if (myType == INTEGER || myType == BOOL){return intVal == rhs.intVal;}
529529
if (myType == DOUBLE){return dblVal == rhs.dblVal;}
530530
if (myType == STRING){return strVal == rhs.strVal;}
531-
if (myType == EMPTY){return true;}
531+
if (myType == EMPTY || myType == UNSET) { return true; }
532532
if (size() != rhs.size()){return false;}
533533
if (myType == OBJECT){
534534
jsonForEachConst(*this, it){
@@ -605,6 +605,11 @@ void JSON::Value::null(){
605605
myType = EMPTY;
606606
}
607607

608+
void JSON::Value::unset() {
609+
null();
610+
myType = UNSET;
611+
}
612+
608613
/// Assigns this JSON::Value to the given JSON::Value, skipping given member recursively.
609614
JSON::Value &JSON::Value::assignFrom(const Value &rhs, const std::set<std::string> &skip){
610615
null();
@@ -634,7 +639,7 @@ JSON::Value &JSON::Value::assignFrom(const Value &rhs, const std::set<std::strin
634639
/// Extends this JSON::Value object with the given JSON::Value object, skipping given member(s) recursively.
635640
JSON::Value &JSON::Value::extend(const Value &rhs, const std::set<std::string> &skip){
636641
// Null values turn into objects automatically for sanity reasons
637-
if (myType == EMPTY){myType = OBJECT;}
642+
if (myType == EMPTY || myType == UNSET) { myType = OBJECT; }
638643
// Abort if either value is not an object
639644
if (myType != rhs.myType || myType != OBJECT){return *this;}
640645
jsonForEachConst(rhs, i){
@@ -737,7 +742,7 @@ JSON::Value::operator double() const{
737742
/// Returns the raw string value if available, otherwise calls toString().
738743
JSON::Value::operator std::string() const{
739744
if (myType == STRING){return strVal;}
740-
if (myType == EMPTY){return "";}
745+
if (myType == EMPTY || myType == UNSET) { return ""; }
741746
return toString();
742747
}
743748

@@ -1100,56 +1105,54 @@ std::string JSON::Value::toString() const{
11001105
std::stringstream st;
11011106
st << intVal;
11021107
return st.str();
1103-
break;
11041108
}
11051109
case DOUBLE:{
11061110
std::stringstream st;
11071111
st.precision(10);
11081112
st << std::fixed << dblVal;
11091113
return st.str();
1110-
break;
11111114
}
11121115
case BOOL:{
11131116
if (intVal != 0){
11141117
return "true";
11151118
}else{
11161119
return "false";
11171120
}
1118-
break;
11191121
}
11201122
case STRING:{
11211123
return JSON::string_escape(strVal);
1122-
break;
11231124
}
11241125
case ARRAY:{
11251126
std::string tmp = "[";
11261127
if (arrVal.size() > 0){
11271128
jsonForEachConst(*this, i){
1128-
tmp += i->toString();
1129+
std::string aVal = i->toString();
1130+
if (!aVal.size()) { aVal = "null"; }
1131+
tmp += aVal;
11291132
if (i.num() + 1 != arrVal.size()){tmp += ",";}
11301133
}
11311134
}
11321135
tmp += "]";
11331136
return tmp;
1134-
break;
11351137
}
11361138
case OBJECT:{
11371139
std::string tmp2 = "{";
11381140
if (objVal.size() > 0){
11391141
jsonForEachConst(*this, i){
11401142
tmp2 += JSON::string_escape(i.key()) + ":";
1141-
tmp2 += i->toString();
1143+
std::string oVal = i->toString();
1144+
if (!oVal.size()) { oVal = "null"; }
1145+
tmp2 += oVal;
11421146
if (i.num() + 1 != objVal.size()){tmp2 += ",";}
11431147
}
11441148
}
11451149
tmp2 += "}";
11461150
return tmp2;
1147-
break;
11481151
}
1152+
case UNSET: return "";
11491153
case EMPTY:
11501154
default: return "null";
11511155
}
1152-
return "null"; // should never get here...
11531156
}
11541157

11551158
/// Converts this JSON::Value to valid JSON notation and returns it.
@@ -1160,31 +1163,27 @@ std::string JSON::Value::toPrettyString(size_t indentation) const{
11601163
std::stringstream st;
11611164
st << intVal;
11621165
return st.str();
1163-
break;
11641166
}
11651167
case DOUBLE:{
11661168
std::stringstream st;
11671169
st.precision(10);
11681170
st << std::fixed << dblVal;
11691171
return st.str();
1170-
break;
11711172
}
11721173
case BOOL:{
11731174
if (intVal != 0){
11741175
return "true";
11751176
}else{
11761177
return "false";
11771178
}
1178-
break;
11791179
}
11801180
case STRING:{
11811181
for (uint8_t i = 0; i < 201 && i < strVal.size(); ++i){
1182-
if (strVal[i] < 32 || strVal[i] > 126 || strVal.size() > 200){
1182+
if (strVal[i] < 32 || strVal[i] > 126 || strVal.size() > 1024) {
11831183
return "\"" + JSON::Value((int64_t)strVal.size()).asString() + " bytes of data\"";
11841184
}
11851185
}
11861186
return JSON::string_escape(strVal);
1187-
break;
11881187
}
11891188
case ARRAY:{
11901189
if (arrVal.size() > 0){
@@ -1198,7 +1197,6 @@ std::string JSON::Value::toPrettyString(size_t indentation) const{
11981197
}else{
11991198
return "[]";
12001199
}
1201-
break;
12021200
}
12031201
case OBJECT:{
12041202
if (objVal.size() > 0){
@@ -1216,12 +1214,11 @@ std::string JSON::Value::toPrettyString(size_t indentation) const{
12161214
}else{
12171215
return "{}";
12181216
}
1219-
break;
12201217
}
1218+
case UNSET: return "";
12211219
case EMPTY:
12221220
default: return "null";
12231221
}
1224-
return "null"; // should never get here...
12251222
}
12261223

12271224
/// Appends the given value to the end of this JSON::Value array.
@@ -1344,7 +1341,7 @@ bool JSON::Value::isArray() const{
13441341

13451342
/// Returns true if this object is null.
13461343
bool JSON::Value::isNull() const{
1347-
return (myType == EMPTY);
1344+
return myType == EMPTY || myType == UNSET;
13481345
}
13491346

13501347
/// Returns the total of the objects and array size combined.
@@ -1392,7 +1389,7 @@ JSON::Value JSON::fromDTMI(const char *data, uint64_t len, uint32_t &i){
13921389
/// \param i Current parsing position in the raw data (defaults to 0).
13931390
/// \param ret Will be set to JSON::Value, parsed from the raw data.
13941391
void JSON::fromDTMI(const char *data, uint64_t len, uint32_t &i, JSON::Value &ret){
1395-
ret.null();
1392+
ret.unset();
13961393
if (i >= len){return;}
13971394
switch (data[i]){
13981395
case 0x01:{// integer
@@ -1429,7 +1426,7 @@ void JSON::fromDTMI(const char *data, uint64_t len, uint32_t &i, JSON::Value &re
14291426
uint16_t tmpi = Bit::btohs(data + i); // set tmpi to the UTF-8 length
14301427
std::string tmpstr = std::string(data + i + 2, tmpi); // set the string data
14311428
i += tmpi + 2; // skip length+size forwards
1432-
ret[tmpstr].null();
1429+
ret[tmpstr].unset();
14331430
fromDTMI(data, len, i,
14341431
ret[tmpstr]); // add content, recursively parsed, updating i, setting indice to tmpstr
14351432
}

lib/json.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ static const std::set<std::string> emptyset;
1515
namespace JSON{
1616

1717
/// Lists all types of JSON::Value.
18-
enum ValueType{EMPTY, BOOL, INTEGER, DOUBLE, STRING, ARRAY, OBJECT};
18+
enum ValueType { EMPTY, BOOL, INTEGER, DOUBLE, STRING, ARRAY, OBJECT, UNSET };
1919

2020
/// JSON-string-escapes a value
2121
std::string string_escape(const std::string &val);
@@ -115,6 +115,7 @@ namespace JSON{
115115
bool isNull() const;
116116
uint32_t size() const;
117117
void null();
118+
void unset();
118119
};
119120

120121
Value fromDTMI2(const std::string &data);

test/json.cpp

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
11
#include "../lib/json.cpp"
2+
23
#include <iostream>
3-
#include <sstream>
44
#include <string>
55

66
int main(int argc, char **argv){
7-
std::string line;
8-
std::stringstream jData;
9-
while (getline(std::cin, line)){jData << line;}
10-
JSON::Value J = JSON::fromString(jData.str().data(), jData.str().size());
7+
JSON::Value J;
8+
// If JSON_STRING is set, parse it as JSON.
9+
if (getenv("JSON_STRING")) {
10+
J = JSON::fromString(getenv("JSON_STRING"));
11+
} else {
12+
// Otherwise, read from stdin
13+
J = JSON::Value(std::cin);
14+
}
15+
std::cout << J.toString() << std::endl;
1116
std::cout << J.toPrettyString() << std::endl;
17+
// If JSON_RESULT is set, compare the toString output to it, return 1 if they do not match, printing the two
18+
if (getenv("JSON_RESULT")) {
19+
if (J.toString() != getenv("JSON_RESULT")) {
20+
std::cerr << "Result '" << J.toString() << "' does not match expected '" << getenv("JSON_RESULT") << "'" << std::endl;
21+
return 1;
22+
}
23+
}
1224
return 0;
1325
}
1426

test/meson.build

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
# Testing binaries that are not unit tests, but intended for manual use
33
convertertest = executable('convertertest', 'converter.cpp', header_tgts, dependencies: libmist_dep)
44
urireadertest = executable('urireadertest', 'urireader.cpp', header_tgts, dependencies: libmist_dep)
5-
jsontest = executable('jsontest', 'json.cpp', header_tgts, dependencies: libmist_dep)
65
resolvetest = executable('resolvetest', 'resolve.cpp', header_tgts, dependencies: libmist_dep)
76
streamstatustest = executable('streamstatustest', 'status.cpp', header_tgts, dependencies: libmist_dep)
87
websockettest = executable('websockettest', 'websocket.cpp', header_tgts, dependencies: libmist_dep)
@@ -33,6 +32,23 @@ test('Retrieve stdout after fail', proctest, suite: 'Procs', args: ['5', '', '',
3332
shellsplittest = executable('shellsplittest', 'shellsplit.cpp', header_tgts, dependencies: libmist_dep)
3433
test('Shell argument splitter', shellsplittest, args: ['hi "a banana" "" sauce pudding "miauw"\'"\' "a"\'\'"z"'], env: {'OUTC':'7', 'OUT1':'hi', 'OUT2':'a banana', 'OUT3':'', 'OUT4':'sauce', 'OUT5':'pudding', 'OUT6':'miauw"', 'OUT7':'az'})
3534

35+
jsontest = executable('jsontest', 'json.cpp', header_tgts, dependencies: libmist_dep)
36+
test('Empty array', jsontest, suite: 'JSON parser / printer', env : {'JSON_STRING':'[]', 'JSON_RESULT':'[]'})
37+
test('Empty object', jsontest, suite: 'JSON parser / printer', env : {'JSON_STRING':'{}', 'JSON_RESULT':'{}'})
38+
test('Blank string', jsontest, suite: 'JSON parser / printer', env : {'JSON_STRING':'', 'JSON_RESULT':''})
39+
test('Newline', jsontest, suite: 'JSON parser / printer', env : {'JSON_STRING':'\n', 'JSON_RESULT':''})
40+
test('Carriage return, newline', jsontest, suite: 'JSON parser / printer', env : {'JSON_STRING':'\r\n', 'JSON_RESULT':''})
41+
test('Array with carriage return and newline mixed', jsontest, suite: 'JSON parser / printer', env : {'JSON_STRING':'\r\n[\n1\n,\n2\r\n]', 'JSON_RESULT':'[1,2]'})
42+
test('Large integer', jsontest, suite: 'JSON parser / printer', env : {'JSON_STRING':'123456789000', 'JSON_RESULT':'123456789000'})
43+
test('11 slices of Pi', jsontest, suite: 'JSON parser / printer', env : {'JSON_STRING':'3.1415926536', 'JSON_RESULT':'3.1415926536'})
44+
test('String with unicode', jsontest, suite: 'JSON parser / printer', env : {'JSON_STRING':'"(\u256F\u00B0\u25A1\u00B0\uFF09\u256F\uFE35 \u253B\u2501\u253B"', 'JSON_RESULT':'"(\\u256F\\u00B0\\u25A1\\u00B0\\uFF09\\u256F\\uFE35 \\u253B\\u2501\\u253B"'})
45+
test('Array of various bools and nulls', jsontest, suite: 'JSON parser / printer', env : {'JSON_STRING':'[n,n,t,t,f,n,f,n,n]', 'JSON_RESULT':'[null,null,true,true,false,null,false,null,null]'})
46+
test('Unclosed array', jsontest, suite: 'JSON parser / printer', env : {'JSON_STRING':'[[]', 'JSON_RESULT':'[[]]'})
47+
test('Invalid symbols', jsontest, suite: 'JSON parser / printer', env : {'JSON_STRING':'-inf', 'JSON_RESULT':'null'})
48+
test('Invalid symbols in array', jsontest, suite: 'JSON parser / printer', env : {'JSON_STRING':'[-inf]', 'JSON_RESULT':'[null]'})
49+
test('Complex structure', jsontest, suite: 'JSON parser / printer', env : {'JSON_STRING':'[{"a":[1,2,3.14]},n,[t,f]]', 'JSON_RESULT':'[{"a":[1,2,3.1400000000]},null,[true,false]]'})
50+
test('Empty object member', jsontest, suite: 'JSON parser / printer', env : {'JSON_STRING':'{"test":}', 'JSON_RESULT':'{"test":null}'})
51+
3652
urltest = executable('urltest', 'url.cpp', header_tgts, dependencies: libmist_dep)
3753
urltest_vm = {'T_PATH':'', 'T_QUERY':'', 'T_FRAG':'', 'T_USER':'', 'T_PASS':'', 'T_NORM':'', 'T_EXT':''}
3854
urltest_v = urltest_vm + {'T_PROTO':'', 'T_HOST':'', 'T_PORT':'0'}

0 commit comments

Comments
 (0)