commit 22e60ee228dff6fd276146e5b7bb27703f339bdf Author: ari melody Date: Sun May 19 23:13:32 2024 +0100 i can't believe i just wrote a json parser in C diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e660fd9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +bin/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..f17e3c3 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# arimelody.me site generator + +i use-a the json to make-a the html 🤌 🇮🇹 + +## how to use + +- run the `build.sh` script to uh. build the thing +- run `./bin/compile -t music -i ./test.json` +- ??? +- profit diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..0f2e841 --- /dev/null +++ b/build.sh @@ -0,0 +1,3 @@ +mkdir -p ./bin/ + +gcc -Wall -o ./bin/compile ./src/*.c ./src/parsers/*.c diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..e0bc1a1 --- /dev/null +++ b/src/main.c @@ -0,0 +1,108 @@ +#include +#include +#include +#include "parsers/json.h" + +typedef enum { + music = 0, + art = 1, + blog = 2, +} MediaType; + +MediaType read_media_type(char *name) { + char *type_names[] = {"music", "art", "blog"}; + for (int i = 0; i < sizeof(type_names) / sizeof(char*); i++) { + if (strcmp(name, type_names[i]) == 0) return i; + } + return -1; +} + +void print_help() { + printf("usage: compile <-t media_type> <-i input_file> [output_file]\n"); + printf("valid media types: music, art, blog\n"); + printf("if output_file is not specified, the output will be written to stdout.\n"); +} + +int main(int argc, char **argv) { + if (argc <= 1) { + print_help(); + return 0; + } + + MediaType media_type = -1; + FILE *input_file = NULL; + size_t optind = 0; + + for (optind = 1; optind < argc && argv[optind][0] == '-'; optind++) { + switch (argv[optind][1]) { + case 't': + media_type = read_media_type(argv[++optind]); + if (media_type < 0) { + fprintf(stderr, "media type `%s` is not implemented!\n\n", argv[optind]); + print_help(); + exit(EXIT_FAILURE); + } + break; + case 'i': + if (optind++ >= argc) { + fprintf(stderr, "-i was passed, but no file path was given!\n\n"); + exit(EXIT_FAILURE); + } + input_file = fopen(argv[optind], "r"); + if (input_file == NULL) { + fprintf(stderr, "failed to open file %s!\n\n", argv[optind]); + exit(EXIT_FAILURE); + } + break; + default: + print_help(); + exit(EXIT_FAILURE); + } + } + + if (media_type < 0) { + fprintf(stderr, "media type not provided!\n\n"); + print_help(); + exit(EXIT_FAILURE); + } + if (input_file == NULL) { + fprintf(stderr, "input file not provided!\n\n"); + print_help(); + exit(EXIT_FAILURE); + } + + // get length of file + fseek(input_file, -sizeof(char), SEEK_END); + size_t length = ftell(input_file); + fseek(input_file, 0, SEEK_SET); + + // read file to buffer + char buf[length + 1]; + fread(buf, 1, length, input_file); + buf[length] = '\0'; + + size_t offset = 0; + JsonObject *json = json_parse(buf, &offset); + if (json == NULL) { + fprintf(stderr, "failed to parse input file!\n"); + exit(EXIT_FAILURE); + } + + printf("title: %s\n", (char*)json_get(json, "title")); + printf("type: %s\n", (char*)json_get(json, "type")); + printf("year: %.0f\n", *(float*)json_get(json, "year")); + printf("artwork: %s\n", (char*)json_get(json, "artwork")); + printf("buylink: %s\n", (char*)json_get(json, "buylink")); + + JsonArrayItem *links = json_get(json, "links"); + size_t i = 0; + while (links != NULL) { + JsonObject *link = links->value; + printf("links.%zu.title: %s\n", i, (char*)json_get(link, "title")); + printf("links.%zu.url: %s\n", i, (char*)json_get(link, "url")); + links = links->next; + i++; + } + + return 0; +} diff --git a/src/parsers/json.c b/src/parsers/json.c new file mode 100644 index 0000000..757b4b8 --- /dev/null +++ b/src/parsers/json.c @@ -0,0 +1,359 @@ +#include "json.h" +#include +#include +#include +#include + +#define SKIP_WHITESPACE while ((data[i] == ' ' || data[i] == '\t' || data[i] == '\n') && data[i] != '\0') i++ + +void json_log(const char *error, char *data, size_t index) { + int cur = 0; + int line_num = 0; + int line_pos = 0; + + // get line number and position + while (cur < index) { + line_pos++; + if (data[cur] == '\n') { + line_pos = 0; + line_num++; + } + cur++; + } + + // get line length + int line_len = line_pos; + while (data[cur] != '\0' && data[cur] != '\n') { + line_len++; + cur++; + } + + // get line + char *line = malloc(sizeof(char) * line_len); + memcpy(line, data + (index - line_pos) * sizeof(char), line_len * sizeof(char)); + + fprintf(stderr, "%s\n", error); + fprintf(stderr, "\tat %d:%d:\n", line_num + 1, line_pos + 1); + fprintf(stderr, "%s\n", line); + for (int i = 0; i < line_pos; i++) fprintf(stderr, " "); + fprintf(stderr, "^\n\n"); +} + +JsonArrayItem *parse_array(char *data, size_t *offset); +void *parse_value(JsonType *type, char *data, size_t *offset); + +JsonArrayItem *parse_array(char *data, size_t *offset) { + size_t i = *offset + 1; + size_t count = 0; + JsonArrayItem *head = NULL; + while (data[i] != '\0' && data[i] != ']') { + void *item_value = NULL; + JsonType type = -1; + + item_value = parse_value(&type, data, &i); + if (item_value == NULL) return NULL; + + if (type == object) { + JsonObject *obj = item_value; + + if (obj->name != NULL) free(obj->name); + obj->name = malloc(sizeof(char) * log10(count) + 1); + sprintf(obj->name, "%zu", count); + } + + if (head == NULL) { + head = malloc(sizeof(JsonArrayItem)); + ((JsonArrayItem*)head)->value = item_value; + } else { + JsonArrayItem *last = head; + while (last->next != NULL) last = last->next; + last->next = malloc(sizeof(JsonArrayItem)); + last->next->value = item_value; + } + + SKIP_WHITESPACE; + // skip comma, if there is one + if (data[i] == ',') i++; + } + + if (data[i] == '\0') { + json_log("error: array was not closed", data, *offset); + return NULL; + } + + *offset = i + 1; + + return head; +} + + + +void *parse_value(JsonType *type, char *data, size_t *offset) { + size_t i = *offset; + *type = -1; + + // infer type from first character + SKIP_WHITESPACE; + switch (data[i]) { + case '{': *type = object; break; + case '[': *type = array; break; + case '"': *type = string; break; + case '0': case '1': case '2': + case '3': case '4': case '5': + case '6': case '7': case '8': + case '9': *type = number; break; + default: { + json_log("error: malformed json value", data, i); + return NULL; + } + } + + void *value = NULL; + switch (*type) { + case object: + { + value = json_parse(data, &i); + i++; + break; + } + case array: + { + value = parse_array(data, &i); + break; + } + case number: + { + float number = 0.f; + uint64_t decimals = 0; + while ((data[i] >= '0' && data[i] <= '9') || data[i] == '.') { + // check for decimal point + if (data[i] == '.') { + if (decimals != 0) { + // two decimal points in one number?? + json_log("error: malformed number", data, *offset); + return NULL; + } + decimals = 1; + } + + if (decimals <= 0) + number = (number * 10) + (data[i] - '0'); + else + number += (data[i] - '0') * (10 * decimals); + + i++; + } + value = malloc(sizeof(float)); + *(float*)value = number; + + break; + } + case string: + { + // skip first quote + i++; + // lookahead to get name length + size_t length = 0; + while (data[i + length] != '\0' && data[i + length] != '"') { + length++; + } + + // error if string was never closed + if (data[i + length] == '\0') { + json_log("error: malformed json", data, *offset); + return NULL; + } + + // consume string + value = malloc(sizeof(char) * (length + 1)); + memcpy(value, data + sizeof(char) * i, sizeof(char) * length); + ((char*)value)[length] = '\0'; + + // skip over string + i += length + 1; + + break; + } + } + + *offset = i; + return value; +} + +JsonObject *json_parse(char *data, size_t *offset) { + if (data[*offset] != '{') { + json_log("error: malformed json object", data, *offset); + return NULL; + } + + size_t i = *offset + 1; + JsonObject *root = malloc(sizeof(JsonObject)); + root->type = object; + JsonObject *last_child = NULL; + + while (data[i] != '\0' && data[i] != '}') { + // read until a quote is found (name start) + if (data[i++] != '"') { + continue; + } + + // lookahead to get name length + size_t name_length = 0; + while (data[i + name_length] != '\0' && data[i + name_length] != '"') { + name_length++; + } + + // error if string was never closed + if (data[i + name_length] == '\0') { + json_log("error: malformed json", data, i); + return NULL; + } + + // consume string as object name + char *name = malloc(sizeof(char) * (name_length + 1)); + memcpy(name, data + sizeof(char) * i, sizeof(char) * name_length); + name[name_length] = '\0'; + + // skip over object name + i += name_length + 1; + + SKIP_WHITESPACE; + // next character must be ':' + if (data[i] != ':') { + json_log("error: ':' does not succeed attribute definition", data, i); + free(name); + return NULL; + } + i++; + + JsonType type; + void *value = parse_value(&type, data, &i); + + if (value == NULL) { + free(name); + return NULL; + } + + JsonObject *obj; + if (type == object) { + obj = json_parse(data, &i); + } else { + // create new json object + obj = malloc(sizeof(JsonObject)); + obj->name = name; + obj->type = type; + obj->value = value; + } + + // append new object to root's children + if (last_child != NULL) { + last_child->next = obj; + last_child = obj; + } else { + root->children = obj; + last_child = obj; + } + + SKIP_WHITESPACE; + // skip comma, if there is one + if (data[i + 1] == '\0' && data[i + 1] == ',') i++; + } + + if (data[i] != '}') { + json_log("error: json object not closed", data, i); + json_free(root); + root = NULL; + } + + *offset = i; + return root; +} + +void *json_get(JsonObject *obj, const char *query) { + if (obj == NULL) return NULL; + + size_t cur = 0; + JsonType searching_type = object; + + while (query[cur] != '\0') { + size_t attr_len = 0; + while (query[cur + attr_len] != '\0' && query[cur + attr_len] != '.') attr_len++; + char attr[attr_len]; + memcpy(attr, query + cur * sizeof(char), attr_len * sizeof(char)); + + if (searching_type == object) { + obj = obj->children; + while (obj != NULL) { + if (strcmp(obj->name, attr) == 0) { + break; + } + obj = obj->next; + } + } + + if (searching_type == array) { + size_t to_i = 0; + size_t cur = 0; + while (cur < attr_len) { + if (attr[cur] < '0' || attr[cur] > '9') { + fprintf(stderr, "error: attempt to index array with attribute name (%s)\n", attr); + return NULL; + } + to_i = (to_i * 10) + (attr[cur] - '0'); + cur++; + } + + JsonArrayItem *item = obj->value; + size_t i = 0; + while (item != NULL && i < to_i) { + item = item->next; + i++; + } + + if (item == NULL) { + fprintf(stderr, "error: index %zu out of bounds for array %s\n", to_i, attr); + return NULL; + } + + obj = item->value; + } + + if (obj == NULL) { + fprintf(stderr, "error: %s does not exist in json object\n", query); + return NULL; + } + cur += attr_len; + if (query[cur] == '.') { + searching_type = obj->type; + cur++; + } + } + + return obj->value; +} + +void json_free(JsonObject *object) { + if (object == NULL) return; + + if (object->children != NULL) { + json_free(object->children); + } + if (object->next != NULL) { + json_free(object->next); + } + + if (object->name != NULL) free(object->name); + if (object->value != NULL) { + if (object->type == array) { + while (object->value != NULL) { + JsonArrayItem *old = object->value; + json_free(old->value); + object->value = old->next; + free(old); + } + } + free(object->value); + } + + free(object); +} diff --git a/src/parsers/json.h b/src/parsers/json.h new file mode 100644 index 0000000..79d626c --- /dev/null +++ b/src/parsers/json.h @@ -0,0 +1,31 @@ +#ifndef _def_json_parser +#define _def_json_parser + +#include + +typedef enum { + object = 0, + array = 1, + number = 2, + string = 3, +} JsonType; + +typedef struct JsonObject { + char *name; + JsonType type; + void *value; + + struct JsonObject *children; + struct JsonObject *next; +} JsonObject; + +typedef struct JsonArrayItem { + JsonObject *value; + struct JsonArrayItem *next; +} JsonArrayItem; + +JsonObject *json_parse(char *data, size_t *offset); +void *json_get(JsonObject *object, const char *child_name); +void json_free(JsonObject *object); + +#endif // _def_json_parser diff --git a/test.json b/test.json new file mode 100644 index 0000000..5cfb519 --- /dev/null +++ b/test.json @@ -0,0 +1,25 @@ +{ + "title": "Dream", + "type": "single", + "year": 2022, + "artwork": "https://mellodoot.com/img/music_artwork/mellodoot_-_Dream.webp", + "buylink": "https://arimelody.bandcamp.com/track/dream", + "links": [ + { + "title": "Spotify", + "url": "https://open.spotify.com/album/5talRpqzjExP1w6j5LFIAh" + }, + { + "title": "Apple Music", + "url": "https://music.apple.com/ie/album/dream-single/1650037132" + }, + { + "title": "Soundcloud", + "url": "https://soundcloud.com/mellodoot/dream" + }, + { + "title": "YouTube", + "url": "https://www.youtube.com/watch?v=nfFgtMuYAx8" + } + ] +}