Einfacher JSON-Parser in C

Hier ist ein einfacher JSON-Parser für rekursiven Abstieg, der nicht viele zusätzliche Funktionen bietet, obwohl er die hier beschriebene erweiterbare Vektorklasse verwendet ( Einfacher erweiterbarer Vektor in C ). Ich habe weder Optimierungen noch eine übergeordnete Zugriffsschnittstelle implementiert, es ist alles ziemlich einfach. Es gibt auch keinen JSON-Export, sondern nur einen Import.

Die gesamte Quelle ist unter github verfügbar ( https://github.com/HarryDC/JsonParser ). CMake und Testcode enthalten, ich werde nur die Parser-Dateien hier posten.

Mein Hauptinteresse wäre, wenn es idiomatischere Möglichkeiten gibt, Dinge in C zu schreiben. Aber natürlich wird auch jede andere Eingabe immer geschätzt.

Header

#ifndef HS_JSON_H #define HS_JSON_H #include "vector.h" enum json_value_type { TYPE_NULL, TYPE_BOOL, TYPE_NUMBER, TYPE_OBJECT, // Is a vector with pairwise entries, key, value TYPE_ARRAY, // Is a vector, all entries are plain TYPE_STRING, TYPE_KEY }; typedef struct { int type; union { int boolean; double number; char* string; char* key; vector array; vector object; } value; } json_value; // Parse string into structure of json elements and values // return 1 if successful. int json_parse(const char* input, json_value* root); // Free the structure and all the allocated values void json_free_value(json_value* val); // Convert value to string if possible, asserts if not char* json_value_to_string(json_value* value); // Convert value to double if possible asserts if not double json_value_to_double(json_value* value); // Convert value to bool if possible asserts if not int json_value_to_bool(json_value* value); // Convert value to vector if it"s an array asserts if not vector* json_value_to_array(json_value* value); // Convert value to vector if it"s an object, asserts if not vector* json_value_to_object(json_value* value); // Fetch the value with given index from root, asserts if root is not array json_value* json_value_at(const json_value* root, size_t index); // Fetche the value with the given key from root, asserts if root is not object json_value * json_value_with_key(const json_value * root, const char * key); #ifdef BUILD_TEST void json_test_all(void); #endif #endif 

Implementierung

#include "json.h" #include <assert.h> #include <ctype.h> #include <stddef.h> #include <stdlib.h> #include <string.h> static int json_parse_value(const char ** cursor, json_value * parent); static void skip_whitespace(const char** cursor) { if (**cursor == "\0") return; while (iscntrl(**cursor) || isspace(**cursor)) ++(*cursor); } static int has_char(const char** cursor, char character) { skip_whitespace(cursor); int success = **cursor == character; if (success) ++(*cursor); return success; } static int json_parse_object(const char** cursor, json_value* parent) { json_value result = { .type = TYPE_OBJECT }; vector_init(&result.value.object, sizeof(json_value)); int success = 1; while (success && !has_char(cursor, "}")) { json_value key = { .type = TYPE_NULL }; json_value value = { .type = TYPE_NULL }; success = json_parse_value(cursor, &key); success = success && has_char(cursor, ":"); success = success && json_parse_value(cursor, &value); if (success) { vector_push_back(&result.value.object, &key); vector_push_back(&result.value.object, &value); } else { json_free_value(&key); break; } skip_whitespace(cursor); if (has_char(cursor, "}")) break; else if (has_char(cursor, ",")) continue; else success = 0; } if (success) { *parent = result; } else { json_free_value(&result); } return success; return 1; } static int json_parse_array(const char** cursor, json_value* parent) { int success = 1; if (**cursor == "]") { ++(*cursor); return success; } while (success) { json_value new_value = { .type = TYPE_NULL }; success = json_parse_value(cursor, &new_value); if (!success) break; skip_whitespace(cursor); vector_push_back(&parent->value.array, &new_value); skip_whitespace(cursor); if (has_char(cursor, "]")) break; else if (has_char(cursor, ",")) continue; else success = 0; } return success; } void json_free_value(json_value* val) { if (!val) return; switch (val->type) { case TYPE_STRING: free(val->value.string); val->value.string = NULL; break; case TYPE_ARRAY: case TYPE_OBJECT: vector_foreach(&(val->value.array), (void(*)(void*))json_free_value); vector_free(&(val->value.array)); break; } val->type = TYPE_NULL; } int json_is_literal(const char** cursor, const char* literal) { size_t cnt = strlen(literal); if (strncmp(*cursor, literal, cnt) == 0) { *cursor += cnt; return 1; } return 0; } static int json_parse_value(const char** cursor, json_value* parent) { // Eat whitespace int success = 0; skip_whitespace(cursor); switch (**cursor) { case "\0": // If parse_value is called with the cursor at the end of the string // that"s a failure success = 0; break; case """: ++*cursor; const char* start = *cursor; char* end = strchr(*cursor, """); if (end) { size_t len = end - start; char* new_string = malloc((len + 1) * sizeof(char)); memcpy(new_string, start, len); new_string[len] = "\0"; assert(len == strlen(new_string)); parent->type = TYPE_STRING; parent->value.string = new_string; *cursor = end + 1; success = 1; } break; case "{": ++(*cursor); skip_whitespace(cursor); success = json_parse_object(cursor, parent); break; case "[": parent->type = TYPE_ARRAY; vector_init(&parent->value.array, sizeof(json_value)); ++(*cursor); skip_whitespace(cursor); success = json_parse_array(cursor, parent); if (!success) { vector_free(&parent->value.array); } break; case "t": { success = json_is_literal(cursor, "true"); if (success) { parent->type = TYPE_BOOL; parent->value.boolean = 1; } break; } case "f": { success = json_is_literal(cursor, "false"); if (success) { parent->type = TYPE_BOOL; parent->value.boolean = 0; } break; } case "n": success = json_is_literal(cursor, "null"); break; default: { char* end; double number = strtod(*cursor, &end); if (*cursor != end) { parent->type = TYPE_NUMBER; parent->value.number = number; *cursor = end; success = 1; } } } return success; } int json_parse(const char* input, json_value* result) { return json_parse_value(&input, result); } char* json_value_to_string(json_value* value) { assert(value->type == TYPE_STRING); return (char *)value->value.string; } double json_value_to_double(json_value* value) { assert(value->type == TYPE_NUMBER); return value->value.number; } int json_value_to_bool(json_value* value) { assert(value->type == TYPE_BOOL); return value->value.boolean; } vector* json_value_to_array(json_value* value) { assert(value->type == TYPE_ARRAY); return &value->value.array; } vector* json_value_to_object(json_value* value) { assert(value->type == TYPE_OBJECT); return &value->value.object; } json_value* json_value_at(const json_value* root, size_t index) { assert(root->type == TYPE_ARRAY); if (root->value.array.size < index) { return vector_get_checked(&root->value.array,index); } else { return NULL; } } json_value* json_value_with_key(const json_value* root, const char* key) { assert(root->type == TYPE_OBJECT); json_value* data = (json_value*)root->value.object.data; size_t size = root->value.object.size; for (size_t i = 0; i < size; i += 2) { if (strcmp(data[i].value.string, key) == 0) { return &data[i + 1]; } } return NULL; } #ifdef BUILD_TEST #include <stdio.h> void json_test_value_string(void) { printf("json_parse_value_string: "); // Normal parse, skip whitespace const char* string = " \n\t\"Hello World!\""; json_value result = { .type = TYPE_NULL }; assert(json_parse_value(&string, &result)); assert(result.type == TYPE_STRING); assert(result.value.string != NULL); assert(strlen(result.value.string) == 12); assert(strcmp("Hello World!", result.value.string) == 0); json_free_value(&result); // Empty string string = "\"\""; json_parse_value(&string, &result); assert(result.type == TYPE_STRING); assert(result.value.string != NULL); assert(strlen(result.value.string) == 0); json_free_value(&result); printf(" OK\n"); } void json_test_value_number(void) { printf("json_test_value_number: "); const char* string = " 23.4"; json_value result = { .type = TYPE_NULL }; assert(json_parse_value(&string, &result)); assert(result.type == TYPE_NUMBER); assert(result.value.number == 23.4); json_free_value(&result); printf(" OK\n"); } void json_test_value_invalid(void) { printf("json_test_value_invalid: "); { // not a valid value const char* string = "xxx"; json_value result = { .type = TYPE_NULL }; assert(!json_parse_value(&string, &result)); assert(result.type == TYPE_NULL); json_free_value(&result); } { // parse_value at end should fail const char* string = ""; json_value result = { .type = TYPE_NULL }; assert(!json_parse_value(&string, &result)); assert(result.type == TYPE_NULL); json_free_value(&result); } printf(" OK\n"); } void json_test_value_array(void) { printf("json_test_value_array: "); { // Empty Array const char* string = "[]"; json_value result = { .type = TYPE_NULL }; assert(result.value.array.data == NULL); assert(json_parse_value(&string, &result)); assert(result.type = TYPE_ARRAY); assert(result.value.array.data != NULL); assert(result.value.array.size == 0); json_free_value(&result); } { // One Element const char* string = "[\"Hello World\"]"; json_value result = { .type = TYPE_NULL }; assert(result.value.array.data == NULL); assert(json_parse_value(&string, &result)); assert(result.type = TYPE_ARRAY); assert(result.value.array.data != NULL); assert(result.value.array.size == 1); json_value* string_value = (json_value *)result.value.array.data; assert(string_value->type == TYPE_STRING); assert(strcmp("Hello World", string_value->value.string) == 0);; json_free_value(&result); } { // Mutliple Elements const char* string = "[0, 1, 2, 3]"; json_value result = { .type = TYPE_NULL }; assert(result.value.array.data == NULL); assert(json_parse_value(&string, &result)); assert(result.type = TYPE_ARRAY); assert(result.value.array.data != NULL); assert(result.value.array.size == 4); json_free_value(&result); } { // Failure const char* string = "[0, 2,,]"; json_value result = { .type = TYPE_NULL }; assert(result.value.array.data == NULL); assert(result.type == TYPE_NULL); assert(result.value.array.data == NULL); json_free_value(&result); } { // Failure // Shouldn"t need to free, valgrind shouldn"t show leak const char* string = "[0, 2, 0"; json_value result = { .type = TYPE_NULL }; assert(result.value.array.data == NULL); assert(result.type == TYPE_NULL); assert(result.value.array.data == NULL); } printf(" OK\n"); } void json_test_value_object(void) { printf("json_test_value_object: "); { // Empty Object const char* string = "{}"; json_value result = { .type = TYPE_NULL }; assert(result.value.object.data == NULL); assert(json_parse_value(&string, &result)); assert(result.type = TYPE_OBJECT); assert(result.value.array.data != NULL); assert(result.value.array.size == 0); json_free_value(&result); } { // One Pair const char* string = "{ \"a\" : 1 }"; json_value result = { .type = TYPE_NULL }; assert(result.value.object.data == NULL); assert(json_parse_value(&string, &result)); assert(result.type = TYPE_OBJECT); assert(result.value.array.data != NULL); assert(result.value.array.size == 2); json_value* members = (json_value *)result.value.object.data; assert(strcmp(json_value_to_string(members), "a") == 0); ++members; assert(json_value_to_double(members) == 1.0); json_free_value(&result); } { // Multiple Pairs const char* string = "{ \"a\": 1, \"b\" : 2, \"c\" : 3 }"; json_value result = { .type = TYPE_NULL }; assert(result.value.object.data == NULL); assert(json_parse_value(&string, &result)); assert(result.type = TYPE_OBJECT); assert(result.value.array.data != NULL); assert(result.value.array.size == 6); json_value* members = (json_value *)result.value.object.data; assert(strcmp(json_value_to_string(&members[4]), "c") == 0); assert(json_value_to_double(&members[5]) == 3.0); json_free_value(&result); } printf(" OK\n"); } void json_test_value_literal(void) { printf("json_test_values_literal: "); { const char* string = "true"; json_value result = { .type = TYPE_NULL }; assert(json_parse_value(&string, &result)); assert(result.type == TYPE_BOOL); assert(result.value.boolean); json_free_value(&result); } { const char* string = "false"; json_value result = { .type = TYPE_NULL }; assert(json_parse_value(&string, &result)); assert(result.type == TYPE_BOOL); assert(!result.value.boolean); json_free_value(&result); } { const char* string = "null"; json_value result = { .type = TYPE_NULL }; assert(json_parse_value(&string, &result)); assert(result.type == TYPE_NULL); json_free_value(&result); } printf(" OK\n"); } const char* test_string_valid = " \ { \"item1\" : [1, 2, 3, 4], \ \"item2\" : { \"a\" : 1, \"b\" : 2, \"c\" : 3 }, \ \"item3\" : \"An Item\" \ }"; const char* test_string_invalid = " \ { \"item1\" : [1, 2, 3, 4], \ \"item2\" : { \"a\" : 1, \"b\" : 2, \"c\" : 3 }, \ \"item3\" , \"An Item\" \ }"; void json_test_coarse(void) { printf("json_test_coarse: "); json_value root; assert(json_parse(test_string_valid, &root)); json_value* val = json_value_with_key(&root, "item1"); assert(root.type == TYPE_OBJECT); assert(root.value.object.size == 6); assert(val != NULL); assert(json_value_to_array(val) != NULL); assert(json_value_to_array(val)->size == 4); val = json_value_with_key(&root, "item3"); assert(val != NULL); assert(json_value_to_string(val) != NULL); assert(strcmp(json_value_to_string(val), "An Item") == 0); json_free_value(&root); // valgrind check for releasing intermediary data assert(!json_parse(test_string_invalid, &root)); printf(" OK\n"); } void json_test_all(void) { json_test_value_invalid(); json_test_value_string(); json_test_value_number(); json_test_value_array(); json_test_value_object(); json_test_value_literal(); json_test_coarse(); } #endif 

Antwort

Header

In C haben alle enum Namen denselben Namespace miteinander ( und mit Dingen wie Variablennamen). Es ist daher eine gute Idee, das Risiko einer Kollision zu verringern.

Ihre enum json_value_type -Namen haben das Präfix , was ziemlich allgemein ist. Eine andere Bibliothek versucht möglicherweise, denselben Namen zu verwenden. Ich würde vorschlagen, dieses Präfix beispielsweise in JSON_ zu ändern.

Außerdem scheinen Sie TYPE_KEY für alles. Entfernen Sie es einfach.

Implementierung

Wie Roland Illig bemerkt , sind die Argumente für iscntrl() und isspace() in Ihrer Funktion skip_whitespace() sollten in unsigned char to umgewandelt werden Vermeiden Sie Zeichenerweiterungen.

Alternativ und genauer nach der JSON-Spezifikation können Sie diese Funktion einfach wie folgt umschreiben:

static void skip_whitespace(const char** cursor) { while (**cursor == "\t" || **cursor == "\r" || **cursor == "\n" || **cursor == " ") ++(*cursor); } 

Viele Ihrer static -Hilfefunktionen führen nicht triviale Kombinationen von Dingen aus und enthalten keinen Kommentar, der erklärt, was sie sind machen. Ein oder zwei Kommentarzeilen vor jeder Funktion können die Lesbarkeit erheblich verbessern.

Insbesondere Ihre has_char() -Funktion führt eine Reihe verschiedener Aufgaben aus:

  1. Leerzeichen werden übersprungen.
  2. Es wird geprüft, ob ein bestimmtes Zeichen in der Eingabe vorhanden ist.
  3. Wenn das Zeichen gefunden wird, wird es automatisch übersprungen.

Nur # 2 wird offensichtlich durch den Funktionsnamen impliziert. Die anderen sind unerwartete Nebenwirkungen und sollten zumindest klar dokumentiert werden.

Eigentlich scheint es mir besser zu sein, den Aufruf von skip_whitespace() zu entfernen von has_char(), und lassen Sie den Aufrufer das Leerzeichen explizit überspringen, bevor Sie es bei Bedarf aufrufen. In vielen Fällen macht Ihr Code dies bereits, wodurch das Überspringen von Duplikaten überflüssig wird.

Um den Effekt Nr. 3 für den Leser weniger überraschend zu machen, ist es möglicherweise eine gute Idee, diese Funktion in etwas mehr umzubenennen aktiv wie beispielsweise read_char().


Am Ende von json_parse_object() haben Sie:

 return success; return 1; } 

Das ist sicherlich überflüssig. Entfernen Sie einfach die return 1;.

Auch Es sieht so aus, als würden Sie die generische Funktion json_parse_value() verwenden, um Objektschlüssel zu analysieren, und nicht testen, um sicherzustellen, dass sie Zeichenfolgen sind. Dadurch kann ein ungültiger JSON durch Ihren Parser gelangen. Ich würde vorschlagen, entweder eine explizite Typprüfung hinzuzufügen oder Ihren String-Parsing-Code in eine separate Funktion (wie unten beschrieben) aufzuteilen und ihn direkt von json_parse_object() aufzurufen.


Oben in json_parse_array() haben Sie:

if (**cursor == "]") { ++(*cursor); return success; } while (success) { 

Sie können das genauso umschreiben wie Sie tun dies in json_parse_object():

while (success && !has_char("]")) { 

(Nur, Sie wissen, ich denke immer noch, der Name read_char() wäre besser.)

Aus irgendeinem Grund scheint Ihre json_parse_array() zu erwarten, dass der Anrufer die parent struct, während json_parse_object() dies automatisch tut. AFAICT Es gibt keinen Grund für die Inkonsistenz, daher könnten und sollten Sie wahrscheinlich beide Funktionen zum Laufen bringen auf die gleiche Weise.


Ihre json_is_literal() -Funktion ist nicht als static markiert, obwohl dies nicht der Fall ist erscheinen in der Kopfzeile. Wie is_char() würde ich al Benennen Sie es daher lieber in etwas Aktiveres um, z. B. json_read_literal() oder nur read_literal(), um klarer zu machen, dass der Cursor automatisch auf a bewegt wird erfolgreiche Übereinstimmung.

(Beachten Sie auch, dass diese Funktion, wie geschrieben, nicht prüft, ob das Literal in der Eingabe tatsächlich dort endet, wo es soll. Beispielsweise würde die Eingabe nullnullnull erfolgreich mit null abgeglichen.Ich glaube nicht, dass dies ein tatsächlicher Fehler ist, da die einzigen gültigen Literale in JSON true, false und null, von denen keines Präfixe voneinander sind, und da zwei Literale in einem gültigen JSON nicht nacheinander ohne ein anderes Token dazwischen erscheinen können. Aber es ist definitiv zumindest erwähnenswert in einem Kommentar.)


Möglicherweise möchten Sie auch einige Ihrer statischen Hilfsfunktionen explizit als inline markieren um dem Compiler einen Hinweis zu geben, dass er versuchen sollte, sie in den aufrufenden Code einzufügen. Ich würde vorschlagen, dies zumindest für skip_whitespace(), has_char() und json_is_literal().

Da Ihre json_value_to_X() Accessorfunktionen alle nur aus einer und eine Zeiger-Dereferenzierung sollten Sie auch in Betracht ziehen, ihre Implementierungen in json.h zu verschieben und sie als static inline zu markieren. Dies würde es dem Compiler ermöglichen, sie auch in anderen .c -Dateien in den aufrufenden Code einzubinden und möglicherweise die assert() beim Aufruf zu optimieren Der Code überprüft den Typ ohnehin bereits.


In Ihrer Hauptfunktion json_parse() möchten Sie möglicherweise explizit überprüfen, ob im Leerzeichen nur noch Leerzeichen vorhanden sind Eingabe, nachdem der Stammwert analysiert wurde.

String-Analyse

Ihr String-Parsing-Code in json_parse_value() ist fehlerhaft, da dies nicht der Fall ist Griff Backslash entweicht. Bei der folgenden gültigen JSON-Eingabe schlägt dies beispielsweise fehl:

"I say: \"Hello, World!\"" 

Möglicherweise möchten Sie dies als Testfall hinzufügen.

Sie sollte auch testen, ob Ihr Code andere Backslash-Escape-Sequenzen wie \b, \f, \n, \r, \t, \/ und insbesondere \\ und \unnnn. Hier sind einige weitere Testfälle für diese:

"\"\b\f\n\r\t\/\\" "void main(void) {\r\n\tprintf(\"I say: \\\"Hello, World!\\\"\\n\");\r\n}" "\u0048\u0065\u006C\u006C\u006F\u002C\u0020\u0057\u006F\u0072\u006C\u0064\u0021" "\u3053\u3093\u306B\u3061\u306F\u4E16\u754C" 

Da JSON-Zeichenfolgen beliebige Unicode-Zeichen enthalten können, müssen Sie entscheiden, wie sie behandelt werden sollen. Die wahrscheinlich einfachste Wahl wäre, Ihre Eingabe und Ausgabe als UTF-8 (oder WTF-8 ) zu deklarieren und entweicht in UTF-8-Byte-Sequenzen (und optional umgekehrt). Da Sie nullterminierte Zeichenfolgen verwenden, möchten Sie möglicherweise lieber \u0000 in die überlange Codierung "\xC0\x80" anstelle eines Null-Bytes.


Damit die Hauptfunktion json_parse_value() lesbar bleibt, Ich würde dringend empfehlen, den String-Parsing-Code in eine separate Hilfsfunktion aufzuteilen. Insbesondere da die korrekte Verarbeitung von Backslash-Escapezeichen dies erheblich erschwert.

Eine der Komplikationen besteht darin, dass Sie nicht genau wissen, wie lange die Die Zeichenfolge wird so lange sein, bis Sie sie analysiert haben. Eine Möglichkeit, damit umzugehen, besteht darin, die zugewiesene Ausgabezeichenfolge mit realloc() dynamisch zu vergrößern, z. B. wie folgt:

// resize output buffer *buffer to new_size bytes // return 1 on success, 0 on failure static int resize_buffer(char** buffer, size_t new_size) { char *new_buffer = realloc(*buffer, new_size); if (new_buffer) { *buffer = new_buffer; return 1; } else return 0; } // parse a JSON string value // expects the cursor to point after the initial double quote // return 1 on success, 0 on failure static int json_parse_string(const char** cursor, json_value* parent) { int success = 1; size_t length = 0, allocated = 8; // start with an 8-byte buffer char *new_string = malloc(allocated); if (!new_string) return 0; while (success && **cursor != """) { if (**cursor == "\0") { success = 0; // unterminated string } // we"re going to need at least one more byte of space while (success && length + 1 > allocated) { success = resize_buffer(&new_string, allocated *= 2); } if (!success) break; if (**cursor != "\\") { new_string[length++] = **cursor; // just copy normal bytes to output ++(*cursor); } else switch ((*cursor)[1]) { case "\\":new_string[length++] = "\\"; *cursor += 2; break; case "/": new_string[length++] = "/"; *cursor += 2; break; case """: new_string[length++] = """; *cursor += 2; break; case "b": new_string[length++] = "\b"; *cursor += 2; break; case "f": new_string[length++] = "\f"; *cursor += 2; break; case "n": new_string[length++] = "\n"; *cursor += 2; break; case "r": new_string[length++] = "\r"; *cursor += 2; break; case "t": new_string[length++] = "\t"; *cursor += 2; break; case "u": // TODO: handle Unicode escapes! (decode to UTF-8?) // note that this may require extending the buffer further default: success = 0; break; // invalid escape sequence } } success = success && resize_buffer(&new_string, length+1); if (!success) { free(new_string); return 0; } new_string[length] = "\0"; parent->type = TYPE_STRING; parent->value.string = new_string; ++(*cursor); // move cursor after final double quote return 1; } 

Eine alternative Lösung wäre, zwei Parsing-Durchgänge über die Eingabe auszuführen: einen, um nur die Länge der Ausgabezeichenfolge zu bestimmen, und einen, um sie tatsächlich zu dekodieren. Dies wäre am einfachsten so etwas wie:

static int json_parse_string(const char** cursor, json_value* parent) { char *tmp_cursor = *cursor; size_t length = (size_t)-1; if (!json_string_helper(&tmp_cursor, &length, NULL)) return 0; char *new_string = malloc(length); if (!new_string) return 0; if (!json_string_helper(&tmp_cursor, &length, new_string)) { free(new_string); return 0; } parent->type = TYPE_STRING; parent->value.string = new_string; *cursor = tmp_cursor; return 1; } 

wobei die Hilfsfunktion:

static int json_parse_helper(const char** cursor, size_t* length, char* new_string) { // ... } 

analysiert eine JSON-Zeichenfolge von höchstens *length Bytes in new_string und schreibt die tatsächliche Länge der analysierten Zeichenfolge in *length oder Wenn new_string == NULL, wird nur die Länge der Zeichenfolge bestimmt, ohne die decodierte Ausgabe tatsächlich irgendwo zu speichern.

Parsing von Zahlen

Ihre aktuelle json_parse_value() Die Implementierung behandelt Zahlen als Standardfall und füttert einfach alles, was nicht mit ", [, {, n, t oder f in die C-Standardbibliotheksfunktion strtod().

Da strtod() eine Obermenge gültiger JSON-Nummernliterale, dies sollte funktionieren, kann aber dazu führen, dass Ihr Code manchmal ac wird cept ungültigen JSON als gültig. Ihr Code akzeptiert beispielsweise +nan, -nan, +inf und -inf als gültige Zahlen und akzeptiert auch hexadezimale Notation wie 0xABC123. Wie in der Dokumentation zu strtod() oben angegeben:

In einem anderen Gebietsschema als dem Standard „C“ Bei „POSIX“ -Lokalen kann diese Funktion zusätzliche, vom Gebietsschema abhängige Syntax erkennen.

Wenn Sie strenger sein möchten, möchten Sie möglicherweise alles, was wie eine Zahl aussieht, explizit anhand der JSON-Grammatik vor der Übergabe an strtod().

Beachten Sie auch, dass strtod() möglicherweise errno z Wenn die Eingabenummer außerhalb des Bereichs von double liegt. Sie sollten dies wahrscheinlich überprüfen.

Testen

Ich habe Ihre Tests nicht im Detail betrachtet, aber es ist großartig zu sehen, dass Sie sie haben (auch wenn, wie angegeben oben könnte ihre Abdeckung verbessert werden.

Persönlich würde ich es jedoch vorziehen, die Tests aus der Implementierung in eine separate Quelldatei zu verschieben. Dies hat sowohl Vor- als auch Nachteile:

  • Der Hauptnachteil besteht darin, dass Sie statische Hilfsfunktionen nicht mehr direkt testen können. Da Ihre öffentliche API jedoch sauber und umfassend aussieht und keine Probleme mit dem „versteckten Status“ aufweist, die das Testen erschweren würden, sollten Sie in der Lage sein, auch nur über die API eine gute Testabdeckung zu erzielen.
  • Der Hauptvorteil (neben einer sauberen Trennung zwischen Implementierungs- und Testcode) besteht darin, dass Ihre Tests die öffentliche API automatisch testen. Insbesondere werden Probleme mit dem Header json.h in angezeigt Wenn Sie Ihre Tests über die API durchführen, können Sie außerdem sicherstellen, dass Ihre API wirklich vollständig und flexibel genug für den allgemeinen Gebrauch ist.

Wenn Sie Ihre statischen Funktionen wirklich noch direkt testen möchten, Sie können jederzeit ein Präprozessor-Flag hinzufügen, das sie optional zum Testen verfügbar macht, entweder über einfache Wrapper oder indem Sie einfach das Schlüsselwort static aus ihren Definitionen entfernen.

Ps. I. habe bemerkt, dass Ihr json_test_value_number() -Test für mich vermutlich fehlschlägt (GCC 5.4.0, i386 arch) weil die Zahl 23.4 im Gleitkomma nicht genau darstellbar ist. Wenn Sie es auf 23.5 ändern, besteht der Test.

Kommentare

  • Großartige Arbeit, danke, offensichtlich habe ich ‚ Überprüfen Sie den Standard nicht so oft, wie ich sollte. Obwohl ich utf-8 absichtlich ignoriert habe (hätte das wahrscheinlich erwähnen sollen.
  • OK, fair genug. Sie sollten wahrscheinlich immer noch zumindest einfache Escapezeichen der Form \u00XX, da einige Encoder sie möglicherweise auch für ASCII-Zeichen verwenden. (Außerdem habe ich einen Vorschlag hinzugefügt, um einige Ihrer Funktionen oben als inline zu markieren, da ich dies früher vergessen habe .)
  • Ich habe diesen Teil des Standards völlig übersehen, ja, die Escape-Sequenzen müssen korrekt analysiert werden.

Antwort

Dies ist in keiner Weise eine vollständige Überprüfung, aber ich werde einige Dinge mitteilen, die mir beim Lesen Ihres Codes aufgefallen sind.

Kommentare

While Kommentare sind sicherlich nett, einige Ihrer Inline-Kommentare fügen dem Code nur Rauschen hinzu.

// Eat whitespace int success = 0; skip_whitespace(cursor); 

Erstens ist der Kommentar eine Zeile zu früh. man kann lesen, dass das Leerzeichen verbraucht wird, wenn man sich die Funktion ansieht – der Name beschreibt es perfekt, th Es ist kein zusätzlicher Kommentar erforderlich.

case "\0": // If parse_value is called with the cursor at the end of the string // that"s a failure success = 0; break; 

Auch dieser Kommentar wiederholt nur das, was der Code selbst sagt.


enum json_value_type { TYPE_NULL, TYPE_BOOL, TYPE_NUMBER, TYPE_OBJECT, // Is a vector with pairwise entries, key, value TYPE_ARRAY, // Is a vector, all entries are plain TYPE_STRING, TYPE_KEY }; 

Nun, diese Kommentare sind nicht wirklich nutzlos, da sie dokumentieren, was jeder Wert darstellt. Aber warum nur für TYPE_OBJECT und TYPE_ARRAY – warum nicht für alle Werte? Persönlich habe ich gerade einen Link zu json.org kurz vor diesem enum gesetzt. Ihre Typen sind analog zu die dort, müssen Sie nur dokumentieren, was TYPE_KEY sein soll. Das bringt mich zum nächsten Punkt …

TYPE_KEY

Wenn Sie sich json.org ansehen, sehen Sie, dass ein Objekt besteht Eine Liste von Mitgliedern , die wiederum aus einer Zeichenfolge und einem Wert bestehen. Dies bedeutet, dass Sie TYPE_KEY! Fügen Sie einfach eine neue Struktur für Mitglieder hinzu, die aus einem TYPE_STRING -Wert und einem anderen json-Wert eines beliebigen Typs besteht, und Sie können loslegen haben zB eine Zahl als Schlüssel für einen Wert, der nicht erlaubt ist. Würde auch einige der objektbezogenen Logik schöner machen, wie diese für die Schleife:

for (size_t i = 0; i < size; i += 2) 

Ironischerweise könnte der Schritt dieser for-Schleife tatsächlich einen Kommentar verwenden (warum += 2?), Aber es fehlt einer.

Verschiedenes

case "\0": // If parse_value is called with the cursor at the end of the string // that"s a failure success = 0; break; 

Warum nicht einfach return 0;?


while (iscntrl(**cursor) || isspace(**cursor)) ++(*cursor); 

und

if (success) ++(*cursor); 

und

if (has_char(cursor, "}")) break; else if (has_char(cursor, ",")) continue; 

und einige andere davon. Ich bin es nicht Besonders gern setzen Sie Bedingung und Aussage in die gleiche Zeile, zumal Sie dies nicht konsequent tun.Ich bin damit einverstanden, dies aus Gründen des Kontrollflusses zu tun, wie if (!something) return;, aber es ist immer noch „meh“. Machen Sie es besser richtig und setzen Sie die Anweisung in eine neue Zeile.


Außerdem finde ich, dass Ihr Code einige leere Zeilen verwenden könnte, um „Regionen“ oder wie auch immer Sie sie nennen möchten, zu trennen Beispiel:

json_value key = { .type = TYPE_NULL }; json_value value = { .type = TYPE_NULL }; success = json_parse_value(cursor, &key); success = success && has_char(cursor, ":"); success = success && json_parse_value(cursor, &value); if (success) { vector_push_back(&result.value.object, &key); vector_push_back(&result.value.object, &value); } else { json_free_value(&key); break; } skip_whitespace(cursor); if (has_char(cursor, "}")) break; else if (has_char(cursor, ",")) continue; else success = 0; 

Es gibt eine leere Zeile, die das Setup-and-Parse-Zeug vom Check-and-Return-Zeug trennt, aber Sie können es tun besser.

json_value key = { .type = TYPE_NULL }; json_value value = { .type = TYPE_NULL }; success = json_parse_value(cursor, &key); success = success && has_char(cursor, ":"); success = success && json_parse_value(cursor, &value); if (success) { vector_push_back(&result.value.object, &key); vector_push_back(&result.value.object, &value); } else { json_free_value(&key); break; } skip_whitespace(cursor); if (has_char(cursor, "}")) break; else if (has_char(cursor, ",")) continue; else success = 0; 

Ich finde das viel sauberer. Sie haben einen Block zum Einrichten der Werte, einen Block zum Parsen, einen Block zum Einfügen In den Vektor gibt es einen Block zum Überspringen von Leerzeichen und einen Block zum Abschließen der aktuellen Aktion. Die letzte leere Zeile zwischen skip_whitespace(cursor); und if ... ist umstritten , aber ich bevorzuge es so.


Abgesehen davon fand ich, dass Ihr Code leicht lesbar und verständlich ist. Sie überprüfen ordnungsgemäß auf Fehler und verwenden eine vernünftige Benennung. Abgesehen von der Idiomatizität Nach allem, was ich erwähnt habe, gibt es nichts, was ich als ungewöhnlich oder nicht idomatisch markieren würde.

Antwort

Die Funktionen von ctype.h dürfen nicht mit Argumenten vom Typ , da dies undefiniertes Verhalten hervorrufen kann. Eine gute Erklärung finden Sie in der NetBSD-Dokumentation .

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.