Forked from
card10 / firmware
727 commits behind the upstream repository.
config.c 8.50 KiB
#include "modules/log.h"
#include "modules/config.h"
#include "modules/filesystem.h"
#include "epicardium.h"
#include <assert.h>
#include <stdbool.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <stddef.h>
#define MAX_LINE_LENGTH 80
#define KEYS_PER_BLOCK 16
#define KEY_LENGTH 16
#define NOT_INT_MAGIC 0x80000000
// one key-value pair representing a line in the config
typedef struct {
char key[KEY_LENGTH];
// the value in the config file, if it's an integer.
// for strings it's set to NOT_INT_MAGIC
int value;
// the byte offset in the config file to read the value string
size_t value_offset;
} config_slot;
// a block of 16 config slots
// if more are needed, this becomes a linked list
typedef struct {
config_slot slots[KEYS_PER_BLOCK];
void *next;
} config_block;
static config_block *config_data = NULL;
// returns the config slot for a key name
static config_slot *find_config_slot(const char *key)
{
config_block *current = config_data;
while (current) {
for (int i = 0; i < KEYS_PER_BLOCK; i++) {
config_slot *k = ¤t->slots[i];
if (strcmp(k->key, key) == 0) {
// found what we're looking for
return k;
} else if (*k->key == '\0') {
// found the first empty key
return NULL;
}
}
current = current->next;
}
return NULL;
}
// returns the next available config slot, or allocates a new block if needed
static config_slot *allocate_config_slot()
{
config_block *current;
if (config_data == NULL) {
config_data = malloc(sizeof(config_block));
assert(config_data != NULL);
memset(config_data, 0, sizeof(config_block));
}
current = config_data;
while (true) {
for (int i = 0; i < KEYS_PER_BLOCK; i++) {
config_slot *k = ¤t->slots[i];
if (*k->key == '\0') {
return k;
}
}
// this block is full and there's no next allocated block
if (current->next == NULL) {
current->next = malloc(sizeof(config_block));
assert(current->next != NULL);
memset(current->next, 0, sizeof(config_block));
}
current = current->next;
}
}
// parses an int out of 'value' or returns NOT_INT_MAGIC
static int try_parse_int(const char *value)
{
char *endptr;
size_t len = strlen(value);
int v = strtol(value, &endptr, 0);
if (endptr != (value + len)) {
return NOT_INT_MAGIC;
}
return v;
}
// loads a key/value pair into a new config slot
static void add_config_pair(
const char *key, const char *value, int line_number, size_t value_offset
) {
if (strlen(key) > KEY_LENGTH - 1) {
LOG_WARN(
"card10.cfg",
"line:%d: too long - aborting",
line_number
);
return;
}
config_slot *slot = allocate_config_slot();
strncpy(slot->key, key, KEY_LENGTH);
slot->value = try_parse_int(value);
slot->value_offset = value_offset;
}
// parses one line of the config file
static void
parse_line(char *line, char *eol, int line_number, size_t line_offset)
{
char *line_start = line;
//skip leading whitespace
while (*line && isspace((int)*line))
++line;
char *key = line;
if (*key == '#') {
//skip comments
return;
}
char *eq = strchr(line, '=');
if (!eq) {
if (*key) {
LOG_WARN(
"card10.cfg",
"line %d: syntax error",
line_number
);
}
return;
}
char *e_key = eq - 1;
//skip trailing whitespace in key
while (e_key > key && isspace((int)*e_key))
--e_key;
e_key[1] = '\0';
if (*key == '\0') {
LOG_WARN("card10.cfg", "line %d: empty key", line_number);
return;
}
char *value = eq + 1;
//skip leading whitespace
while (*value && isspace((int)*value))
++value;
char *e_val = eol - 1;
//skip trailing whitespace
while (e_val > value && isspace((int)*e_val))
--e_val;
if (*value == '\0') {
LOG_WARN(
"card10.cfg",
"line %d: empty value for option '%s'",
line_number,
key
);
return;
}
size_t value_offset = value - line_start + line_offset;
add_config_pair(key, value, line_number, value_offset);
}
// convert windows line endings to unix line endings.
// we don't care about the extra empty lines
static void convert_crlf_to_lflf(char *buf, int n)
{
while (n--) {
if (*buf == '\r') {
*buf = '\n';
}
buf++;
}
}
// parses the entire config file
void load_config(void)
{
LOG_DEBUG("card10.cfg", "loading...");
int fd = epic_file_open("card10.cfg", "r");
if (fd < 0) {
LOG_DEBUG(
"card10.cfg",
"loading failed: %s (%d)",
strerror(-fd),
fd
);
return;
}
char buf[MAX_LINE_LENGTH + 1];
int line_number = 0;
size_t file_offset = 0;
int nread;
do {
nread = epic_file_read(fd, buf, MAX_LINE_LENGTH);
convert_crlf_to_lflf(buf, nread);
if (nread < MAX_LINE_LENGTH) {
//add fake EOL to ensure termination
buf[nread++] = '\n';
}
//zero-terminate buffer
buf[nread] = '\0';
char *line = buf;
char *eol = NULL;
int last_eol = 0;
while (line) {
//line points one character past the last (if any) '\n' hence '- 1'
last_eol = line - buf - 1;
eol = strchr(line, '\n');
++line_number;
if (eol) {
*eol = '\0';
parse_line(line, eol, line_number, file_offset);
file_offset += eol - line + 1;
line = eol + 1;
continue;
}
if (line == buf) {
//line did not fit into buf
LOG_WARN(
"card10.cfg",
"line:%d: too long - aborting",
line_number
);
return;
}
int seek_back = last_eol - nread;
LOG_DEBUG(
"card10.cfg",
"nread, last_eol, seek_back: %d,%d,%d",
nread,
last_eol,
seek_back
);
assert(seek_back <= 0);
if (!seek_back) {
break;
}
int rc = epic_file_seek(fd, seek_back, SEEK_CUR);
if (rc < 0) {
LOG_ERR("card10.cfg", "seek failed, aborting");
return;
}
char newline;
rc = epic_file_read(fd, &newline, 1);
if (rc < 0 || (newline != '\n' && newline != '\r')) {
LOG_ERR("card10.cfg", "seek failed, aborting");
LOG_DEBUG(
"card10.cfg",
"seek failed at read-back of newline: rc: %d read: %d",
rc,
(int)newline
);
return;
}
break;
}
} while (nread == MAX_LINE_LENGTH);
epic_file_close(fd);
}
// opens the config file, seeks to seek_offset and reads buf_len bytes
// used for reading strings without storing them in memory
// since we don't need to optimize for that use case as much
static size_t read_config_offset(size_t seek_offset, char *buf, size_t buf_len)
{
int fd = epic_file_open("card10.cfg", "r");
if (fd < 0) {
LOG_DEBUG(
"card10.cfg",
"opening config failed: %s (%d)",
strerror(-fd),
fd
);
return 0;
}
int rc = epic_file_seek(fd, seek_offset, SEEK_SET);
if (rc < 0) {
LOG_ERR("card10.cfg", "seek failed, aborting");
return 0;
}
// one byte less to accommodate the 0 termination
int nread = epic_file_read(fd, buf, buf_len - 1);
buf[nread] = '\0';
epic_file_close(fd);
return nread;
}
// returns error if not found or invalid
int epic_config_get_integer(const char *key, int *value)
{
config_slot *slot = find_config_slot(key);
if (slot && slot->value != NOT_INT_MAGIC) {
*value = slot->value;
return 0;
}
return -ENOENT;
}
// returns default_value if not found or invalid
int config_get_integer_with_default(const char *key, int default_value)
{
int value;
int ret = epic_config_get_integer(key, &value);
if (ret) {
return default_value;
} else {
return value;
}
}
// returns error if not found
int epic_config_get_string(const char *key, char *buf, size_t buf_len)
{
config_slot *slot = find_config_slot(key);
if (!(slot && slot->value_offset)) {
return -ENOENT;
}
size_t nread = read_config_offset(slot->value_offset, buf, buf_len);
if (nread == 0) {
return -ENOENT;
}
char *eol = strchr(buf, '\n');
if (eol) {
*eol = '\0';
}
return 0;
}
// returns dflt if not found, otherwise same pointer as buf
char *config_get_string_with_default(
const char *key, char *buf, size_t buf_len, char *dflt
) {
int ret = epic_config_get_string(key, buf, buf_len);
if (ret) {
return dflt;
} else {
return buf;
}
}
// returns error if not found or invalid
int epic_config_get_boolean(const char *key, bool *value)
{
int int_value;
int ret = epic_config_get_integer(key, &int_value);
if (ret == 0) {
*value = !!int_value;
return 0;
}
char buf[MAX_LINE_LENGTH];
epic_config_get_string(key, buf, MAX_LINE_LENGTH);
if (!strcmp(buf, "true")) {
*value = true;
return 0;
} else if (!strcmp(buf, "false")) {
*value = false;
return 0;
}
return -ERANGE;
}
// returns default_value if not found or invalid
bool config_get_boolean_with_default(const char *key, bool default_value)
{
bool value;
int ret = epic_config_get_boolean(key, &value);
if (ret) {
return default_value;
} else {
return value;
}
}