aboutsummaryrefslogtreecommitdiffstats
path: root/src/config.c
diff options
context:
space:
mode:
authorThomas Vanbesien <tvanbesi@proton.me>2026-02-17 11:07:37 +0100
committerThomas Vanbesien <tvanbesi@proton.me>2026-02-17 11:07:37 +0100
commita54421dd976fd8081e96c11c2621076876c9986b (patch)
treea7614934364bc692dd94ee13a3ec6d242521194b /src/config.c
parentd1e229c80a6e51ccc5b21d001271c41d6cda30bf (diff)
downloadBobinkCOpcUa-a54421dd976fd8081e96c11c2621076876c9986b.tar.gz
BobinkCOpcUa-a54421dd976fd8081e96c11c2621076876c9986b.zip
Replace CLI arguments with config-file parser and add integration tests
Introduce a reusable key=value config parser (config.h/c) and convert all three programs to read their settings from config files instead of positional command-line arguments. Add example config files in config/ and 6 CTest integration tests covering None/Basic256Sha256/Aes128 with anonymous and user authentication. Remove the now-obsolete launch.sh.
Diffstat (limited to 'src/config.c')
-rw-r--r--src/config.c252
1 files changed, 252 insertions, 0 deletions
diff --git a/src/config.c b/src/config.c
new file mode 100644
index 0000000..163f601
--- /dev/null
+++ b/src/config.c
@@ -0,0 +1,252 @@
+/**
+ * @file config.c
+ * @brief Simple key=value configuration file parser implementation.
+ */
+
+#include "config.h"
+
+#include <open62541/plugin/log_stdout.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* Enough for typical config files; doubles on overflow. */
+#define CONFIG_INITIAL_CAPACITY 16
+
+/* Sufficient for file paths and URIs. */
+#define CONFIG_LINE_MAX 1024
+
+/* ========================================================================
+ * Static Helpers
+ * ======================================================================== */
+
+/**
+ * @brief Trims leading and trailing whitespace in place.
+ *
+ * Modifies the string by writing a NUL terminator after the last
+ * non-whitespace character and returns a pointer past any leading
+ * whitespace.
+ */
+static char *
+trim (char *s)
+{
+ while (*s == ' ' || *s == '\t')
+ s++;
+ size_t len = strlen (s);
+ while (len > 0 && (s[len - 1] == ' ' || s[len - 1] == '\t'))
+ len--;
+ s[len] = '\0';
+ return s;
+}
+
+/**
+ * @brief Appends a key-value pair to the config's dynamic array.
+ *
+ * Grows the array by doubling capacity when full.
+ *
+ * @return 0 on success, -1 on allocation failure.
+ */
+static int
+configAppend (Config *cfg, const char *key, const char *value)
+{
+ if (cfg->count == cfg->capacity)
+ {
+ size_t newCap = cfg->capacity * 2;
+ ConfigEntry *tmp = realloc (cfg->entries, newCap * sizeof (ConfigEntry));
+ if (!tmp)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Config: out of memory");
+ return -1;
+ }
+ cfg->entries = tmp;
+ cfg->capacity = newCap;
+ }
+
+ cfg->entries[cfg->count].key = strdup (key);
+ cfg->entries[cfg->count].value = strdup (value);
+ if (!cfg->entries[cfg->count].key || !cfg->entries[cfg->count].value)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Config: out of memory");
+ return -1;
+ }
+ cfg->count++;
+ return 0;
+}
+
+/* ========================================================================
+ * Public API
+ * ======================================================================== */
+
+int
+configLoad (const char *path, Config *cfg)
+{
+ memset (cfg, 0, sizeof (Config));
+
+ cfg->entries = malloc (CONFIG_INITIAL_CAPACITY * sizeof (ConfigEntry));
+ if (!cfg->entries)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Config: out of memory");
+ return -1;
+ }
+ cfg->capacity = CONFIG_INITIAL_CAPACITY;
+
+ FILE *fp = fopen (path, "r");
+ if (!fp)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Config: cannot open '%s'", path);
+ free (cfg->entries);
+ memset (cfg, 0, sizeof (Config));
+ return -1;
+ }
+
+ char line[CONFIG_LINE_MAX];
+ int lineNum = 0;
+
+ while (fgets (line, sizeof (line), fp))
+ {
+ lineNum++;
+
+ /* Strip trailing newline / carriage return. */
+ size_t len = strlen (line);
+ while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r'))
+ line[--len] = '\0';
+
+ /* Skip blank lines and comments. */
+ char *trimmed = trim (line);
+ if (*trimmed == '\0' || *trimmed == '#')
+ continue;
+
+ /* Find the '=' separator. */
+ char *eq = strchr (trimmed, '=');
+ if (!eq)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Config: syntax error at %s:%d "
+ "(missing '=')",
+ path, lineNum);
+ fclose (fp);
+ configFree (cfg);
+ return -1;
+ }
+
+ *eq = '\0';
+ char *key = trim (trimmed);
+ char *value = trim (eq + 1);
+
+ if (*key == '\0')
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Config: empty key at %s:%d", path, lineNum);
+ fclose (fp);
+ configFree (cfg);
+ return -1;
+ }
+
+ if (configAppend (cfg, key, value) != 0)
+ {
+ fclose (fp);
+ configFree (cfg);
+ return -1;
+ }
+ }
+
+ fclose (fp);
+ return 0;
+}
+
+const char *
+configGet (const Config *cfg, const char *key)
+{
+ for (size_t i = 0; i < cfg->count; i++)
+ {
+ if (strcmp (cfg->entries[i].key, key) == 0)
+ return cfg->entries[i].value;
+ }
+ return NULL;
+}
+
+const char *
+configRequire (const Config *cfg, const char *key, const char *program)
+{
+ const char *val = configGet (cfg, key);
+ if (!val)
+ UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "%s: missing required config key '%s'", program, key);
+ return val;
+}
+
+int
+configRequireInt (const Config *cfg, const char *key, const char *program)
+{
+ const char *val = configRequire (cfg, key, program);
+ if (!val)
+ return -1;
+
+ char *endptr;
+ long num = strtol (val, &endptr, 10);
+ if (*endptr != '\0')
+ {
+ UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "%s: config key '%s' is not a valid integer: '%s'",
+ program, key, val);
+ return -1;
+ }
+ return (int)num;
+}
+
+void
+configGetAll (const Config *cfg, const char *key, char ***out, size_t *size)
+{
+ /* First pass: count matches. */
+ size_t count = 0;
+ for (size_t i = 0; i < cfg->count; i++)
+ {
+ if (strcmp (cfg->entries[i].key, key) == 0)
+ count++;
+ }
+
+ if (count == 0)
+ {
+ *out = NULL;
+ *size = 0;
+ return;
+ }
+
+ /* Second pass: collect pointers. */
+ char **arr = malloc (count * sizeof (char *));
+ if (!arr)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Config: out of memory");
+ *out = NULL;
+ *size = 0;
+ return;
+ }
+
+ size_t idx = 0;
+ for (size_t i = 0; i < cfg->count; i++)
+ {
+ if (strcmp (cfg->entries[i].key, key) == 0)
+ arr[idx++] = cfg->entries[i].value;
+ }
+
+ *out = arr;
+ *size = count;
+}
+
+void
+configFree (Config *cfg)
+{
+ for (size_t i = 0; i < cfg->count; i++)
+ {
+ free (cfg->entries[i].key);
+ free (cfg->entries[i].value);
+ }
+ free (cfg->entries);
+ memset (cfg, 0, sizeof (Config));
+}