summaryrefslogtreecommitdiffstats
path: root/src/cfg.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/cfg.c')
-rw-r--r--src/cfg.c464
1 files changed, 464 insertions, 0 deletions
diff --git a/src/cfg.c b/src/cfg.c
new file mode 100644
index 0000000..b8925f9
--- /dev/null
+++ b/src/cfg.c
@@ -0,0 +1,464 @@
+/*
+ * Copyright (C) 2021 Daiki Ueno
+ *
+ * This file is part of GnuTLS.
+ *
+ * GnuTLS is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * GnuTLS 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
+ * <https://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cfg.h"
+
+#include <assert.h>
+#include <ctype.h>
+#include <errno.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "xsize.h"
+
+#define SIZEOF(x) (sizeof(x) / sizeof((x)[0]))
+
+struct options_st {
+ struct cfg_option_st *data;
+ size_t length;
+ size_t capacity;
+};
+
+struct parser_st
+{
+ FILE *fp;
+ char pushback[2];
+ size_t pushback_length;
+};
+
+static inline void
+clear_option(struct cfg_option_st *option)
+{
+ free(option->name);
+ free(option->value);
+ memset(option, 0, sizeof(*option));
+}
+
+void
+cfg_free(cfg_option_t options)
+{
+ for (size_t i = 0; options[i].name; i++) {
+ clear_option(&options[i]);
+ }
+ free(options);
+}
+
+#define HORIZONTAL_WHITESPACE "\t "
+#define WHITESPACE HORIZONTAL_WHITESPACE "\n\v\f\r\b"
+#define ALPHABETIC "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+#define DECIMAL "0123456789"
+#define NAME_FIRST_CHARS "_" ALPHABETIC
+#define VALUE_NAME_CHARS ":^-" NAME_FIRST_CHARS DECIMAL
+
+struct buffer_st {
+ char *data;
+ size_t length;
+ size_t capacity;
+};
+
+static int
+buffer_append(struct buffer_st *buffer, int c)
+{
+ size_t new_length = xsum(buffer->length, 1);
+ if (size_overflow_p(new_length)) {
+ return -EINVAL;
+ }
+ if (buffer->capacity < new_length) {
+ size_t new_capacity;
+ char *new_array;
+
+ new_capacity = xtimes(xsum(buffer->capacity, 1), 2);
+ if (size_overflow_p(new_capacity)) {
+ return -EINVAL;
+ }
+ new_array = realloc(buffer->data, new_capacity);
+ if (!new_array) {
+ return -errno;
+ }
+ buffer->capacity = new_capacity;
+ buffer->data = new_array;
+ }
+ assert(buffer->data);
+ buffer->data[buffer->length++] = c;
+ return 0;
+}
+
+static int
+parser_getc(struct parser_st *parser)
+{
+ if (parser->pushback_length > 0) {
+ return parser->pushback[--parser->pushback_length];
+ }
+ int c = getc(parser->fp);
+ return c;
+}
+
+static void
+parser_ungetc(struct parser_st *parser, int c)
+{
+ assert(parser->pushback_length < SIZEOF(parser->pushback));
+ parser->pushback[parser->pushback_length++] = c;
+}
+
+static void
+skip_comment(struct parser_st *parser)
+{
+ int c;
+
+ c = parser_getc(parser);
+ if (c == EOF) {
+ return;
+ }
+
+ if (c == '#') {
+ for (;;) {
+ c = parser_getc(parser);
+ if (c == EOF) {
+ return;
+ }
+ if (c == '\n') {
+ break;
+ }
+ }
+ }
+ parser_ungetc(parser, c);
+}
+
+static void
+skip_chars(struct parser_st *parser, const char *chars)
+{
+ int c;
+
+ for (;;) {
+ c = parser_getc(parser);
+ if (c == EOF) {
+ return;
+ }
+ if (!strchr(chars, c)) {
+ break;
+ }
+ }
+ parser_ungetc(parser, c);
+}
+
+static void
+skip_comments_and_whitespaces(struct parser_st *parser)
+{
+ int c;
+
+ for (;;) {
+ c = parser_getc(parser);
+ if (c == EOF) {
+ return;
+ }
+ parser_ungetc(parser, c);
+ if (c == '#') {
+ skip_comment(parser);
+ } else if (strchr(WHITESPACE, c)) {
+ skip_chars(parser, WHITESPACE);
+ } else {
+ break;
+ }
+ }
+}
+
+/* Read the name part of an option. Returns NULL if it fails. */
+static char *
+read_name(struct parser_st *parser)
+{
+ struct buffer_st buffer;
+ int c;
+
+ memset(&buffer, 0, sizeof(buffer));
+
+ skip_comments_and_whitespaces(parser);
+
+ c = parser_getc(parser);
+ if (c == EOF) {
+ return NULL;
+ }
+
+ if (!strchr(NAME_FIRST_CHARS, c)) {
+ parser_ungetc(parser, c);
+ return NULL;
+ }
+
+ buffer_append(&buffer, c);
+ for (;;) {
+ c = parser_getc(parser);
+ if (c == EOF) {
+ break;
+ }
+ if (!strchr(VALUE_NAME_CHARS, c)) {
+ parser_ungetc(parser, c);
+ break;
+ }
+ buffer_append(&buffer, c);
+ }
+ assert(buffer.data);
+ if (buffer.data[buffer.length - 1] == ':') {
+ buffer.data[buffer.length - 1] = '\0';
+ buffer.length--;
+ parser_ungetc(parser, ':');
+ }
+
+ /* NUL terminate */
+ buffer_append(&buffer, '\0');
+ return buffer.data;
+}
+
+static char *
+read_quoted_value(struct parser_st *parser)
+{
+ struct buffer_st buffer;
+ int c, quote_char;
+
+ memset(&buffer, 0, sizeof(buffer));
+
+ c = parser_getc(parser);
+ if (c == EOF) {
+ assert(false);
+ return NULL;
+ }
+
+ if (c == '"' || c == '\'') {
+ quote_char = c;
+ } else {
+ assert(false);
+ return NULL;
+ }
+
+ for (;;) {
+ c = parser_getc(parser);
+ if (c == EOF) {
+ break;
+ }
+ if (c == '\\') {
+ c = parser_getc(parser);
+ if (c == EOF) {
+ /* unmatched quote */
+ free(buffer.data);
+ return NULL;
+ }
+ if (c == '\n') {
+ buffer_append(&buffer, ' ');
+ } else if (c == quote_char) {
+ buffer_append(&buffer, c);
+ }
+ } else if (c == quote_char) {
+ break;
+ } else {
+ buffer_append(&buffer, c);
+ }
+ }
+
+ /* NUL terminate */
+ buffer_append(&buffer, '\0');
+ return buffer.data;
+}
+
+/* Read the value part of an option. Returns NULL if it fails. */
+static char *
+read_value(struct parser_st *parser)
+{
+ struct buffer_st buffer;
+ int c;
+
+ memset(&buffer, 0, sizeof(buffer));
+
+ skip_chars(parser, HORIZONTAL_WHITESPACE);
+
+ c = parser_getc(parser);
+ if (c == EOF) {
+ goto out;
+ }
+
+ /* skip delimiter if any, followed by horizontal whitespaces */
+ if (c == ':' || c == '=') {
+ c = parser_getc(parser);
+ if (c == EOF) {
+ goto out;
+ }
+ parser_ungetc(parser, c);
+ skip_chars(parser, HORIZONTAL_WHITESPACE);
+ c = parser_getc(parser);
+ if (c == EOF) {
+ goto out;
+ }
+ }
+
+ if (c == '\n') {
+ return strdup(""); /* empty value */
+ } else if (c == '"' || c == '\'') {
+ parser_ungetc(parser, c);
+ return read_quoted_value(parser);
+ }
+
+ buffer_append(&buffer, c);
+ for (;;) {
+ c = parser_getc(parser);
+ if (c == EOF) {
+ break;
+ }
+ if (c == '\\') {
+ c = parser_getc(parser);
+ if (c == EOF) {
+ break;
+ }
+ if (c == '\n') {
+ buffer_append(&buffer, c);
+ }
+ } else if (c == '\n') {
+ break;
+ } else {
+ buffer_append(&buffer, c);
+ }
+ }
+
+ out:
+ /* NUL terminate */
+ buffer_append(&buffer, '\0');
+ return buffer.data;
+}
+
+/* Append OPTION to OPTIONS. Take ownership of the fields of OPTION. */
+static int
+take_option(struct options_st *options, struct cfg_option_st *option)
+{
+ size_t new_length = xsum(options->length, 1);
+ if (size_overflow_p(new_length)) {
+ return -EINVAL;
+ }
+ if (options->capacity < new_length) {
+ size_t new_capacity;
+ struct cfg_option_st *new_array;
+
+ new_capacity = xtimes(xsum(options->capacity, 1), 2);
+ if (size_overflow_p(new_capacity)) {
+ return -EINVAL;
+ }
+ new_array = reallocarray(options->data, new_capacity,
+ sizeof(*option));
+ if (!new_array) {
+ return -errno;
+ }
+ options->capacity = new_capacity;
+ options->data = new_array;
+ }
+
+ assert(options->data);
+
+ options->data[options->length].name = option->name;
+ options->data[options->length].value = option->value;
+
+ options->length++;
+
+ option->name = NULL;
+ option->value = NULL;
+
+ return 0;
+}
+
+static void
+clear_options(struct options_st *options)
+{
+ for (size_t i = 0; options->length; i++) {
+ clear_option(&options->data[i]);
+ }
+}
+
+cfg_option_t
+cfg_load(const char *filename)
+{
+ struct parser_st parser;
+ struct options_st options;
+ struct cfg_option_st null_option = { NULL, NULL };
+
+ memset(&parser, 0, sizeof(parser));
+ memset(&options, 0, sizeof(options));
+
+ parser.fp = fopen(filename, "r");
+ if (!parser.fp) {
+ return NULL;
+ }
+
+ for (;;) {
+ struct cfg_option_st option;
+
+ option.name = read_name(&parser);
+ if (!option.name) {
+ break;
+ }
+
+ option.value = read_value(&parser);
+ if (!option.value) {
+ clear_option(&option);
+ goto error;
+ }
+
+ if (take_option(&options, &option) < 0) {
+ clear_option(&option);
+ goto error;
+ }
+ assert(!option.name && !option.value);
+ }
+
+ fclose(parser.fp);
+ /* NUL terminate */
+ take_option(&options, &null_option);
+ return options.data;
+
+error:
+ clear_options(&options);
+ fclose(parser.fp);
+ return NULL;
+}
+
+cfg_option_t
+cfg_next(const cfg_option_t options, const char *name)
+{
+ for (size_t i = 0; options[i].name; i++) {
+ if (strcmp(options[i].name, name) == 0) {
+ return &options[i];
+ }
+ }
+ return NULL;
+}
+
+#ifdef TEST
+int
+main(int argc, char **argv)
+{
+ cfg_option_t opts;
+
+ assert(argc == 2);
+
+ opts = cfg_load(argv[1]);
+ for (size_t i = 0; opts[i].name; i++) {
+ printf("%s: %s\n", opts[i].name, opts[i].value);
+ }
+ cfg_free(opts);
+
+ return 0;
+}
+#endif