Commit 1c954b11 authored by Rahix's avatar Rahix
Browse files

Merge 'New config API with support for dynamic keys'

See merge request card10/firmware!339
parents fbeb7766 c8553481
......@@ -143,6 +143,9 @@ typedef _Bool bool;
#define API_WS2812_WRITE 0x0120
#define API_CONFIG_GET_STRING 0x130
#define API_CONFIG_GET_INTEGER 0x131
#define API_CONFIG_GET_BOOLEAN 0x132
/* clang-format on */
typedef uint32_t api_int_id_t;
......@@ -1924,5 +1927,53 @@ API(API_USB_CDCACM, int epic_usb_cdcacm(void));
*/
API(API_WS2812_WRITE, void epic_ws2812_write(uint8_t pin, uint8_t *pixels, uint32_t n_bytes));
/**
* Configuration
* ======
*/
/**
* Read an integer from the configuration file
*
* :param char* key: Name of the option to read
* :param int* value: Place to read the value into
* :return: `0` on success or a negative value if an error occured. Possible
* errors:
*
* - ``-ENOENT``: Value can not be read
*/
API(API_CONFIG_GET_INTEGER, int epic_config_get_integer(const char *key, int *value));
/**
* Read a boolean from the configuration file
*
* :param char* key: Name of the option to read
* :param bool* value: Place to read the value into
* :return: `0` on success or a negative value if an error occured. Possible
* errors:
*
* - ``-ENOENT``: Value can not be read
*/
API(API_CONFIG_GET_BOOLEAN, int epic_config_get_boolean(const char *key, bool *value));
/**
* Read a string from the configuration file.
*
* If the buffer supplied is too small for the config option,
* no error is reported and the first `buf_len - 1` characters
* are returned (0 terminated).
*
* :param char* key: Name of the option to read
* :param char* buf: Place to read the string into
* :param size_t buf_len: Size of the provided buffer
* :return: `0` on success or a negative value if an error occured. Possible
* errors:
*
* - ``-ENOENT``: Value can not be read
*/
API(API_CONFIG_GET_STRING, int epic_config_get_string(const char *key, char *buf, size_t buf_len));
#endif /* _EPICARDIUM_H */
#include "modules/log.h"
#include "modules/config.h"
#include "modules/filesystem.h"
#include "epicardium.h"
#include <assert.h>
#include <stdbool.h>
......@@ -8,172 +9,128 @@
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <stddef.h>
#define CONFIG_MAX_LINE_LENGTH 80
enum OptionType {
OptionType_Boolean,
OptionType_Int,
OptionType_Float,
OptionType_String,
};
struct config_option {
const char *name;
enum OptionType type;
union {
bool boolean;
long integer;
double floating_point;
char *string;
} value;
};
static struct config_option s_options[_EpicOptionCount] = {
/* clang-format off */
#define INIT_Boolean(v) { .boolean = (v) }
#define INIT_Int(v) { .integer = (v) }
#define INIT_Float(v) { .floating_point = (v) }
#define INIT_String(v) { .string = (v) }
#define INIT_(tp, v) INIT_ ## tp (v)
#define INIT(tp, v) INIT_ (tp, v)
#define CARD10_SETTING(identifier, spelling, tp, default_value) \
[Option ## identifier] = { .name = (spelling), \
.type = OptionType_ ## tp, \
.value = INIT(tp, (default_value)) },
#include "modules/config.def"
/* clang-format on */
};
static struct config_option *findOption(const char *key)
#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)
{
for (int i = 0; i < _EpicOptionCount; ++i) {
if (!strcmp(key, s_options[i].name)) {
return &s_options[i];
config_block *current = config_data;
while (current) {
for (int i = 0; i < KEYS_PER_BLOCK; i++) {
config_slot *k = &current->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;
}
static bool set_bool(struct config_option *opt, const char *value)
// returns the next available config slot, or allocates a new block if needed
static config_slot *allocate_config_slot()
{
bool val;
if (!strcmp(value, "1")) {
val = true;
} else if (!strcmp(value, "true")) {
val = true;
} else if (!strcmp(value, "0")) {
val = false;
} else if (!strcmp(value, "false")) {
val = false;
} else {
return false;
config_block *current;
if (config_data == NULL) {
config_data = malloc(sizeof(config_block));
assert(config_data != NULL);
memset(config_data, 0, sizeof(config_block));
}
opt->value.boolean = val;
LOG_DEBUG(
"card10.cfg",
"setting '%s' to %s",
opt->name,
val ? "true" : "false"
);
return true;
}
static bool set_int(struct config_option *opt, const char *value)
{
char *endptr;
size_t len = strlen(value);
int v = strtol(value, &endptr, 0);
if (endptr != (value + len)) {
return false;
current = config_data;
while (true) {
for (int i = 0; i < KEYS_PER_BLOCK; i++) {
config_slot *k = &current->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;
}
opt->value.integer = v;
LOG_DEBUG("card10.cfg", "setting '%s' to %d (0x%08x)", opt->name, v, v);
return true;
}
static bool set_float(struct config_option *opt, const char *value)
// 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);
double v = strtod(value, &endptr);
if (endptr != (value + len)) {
return false;
}
opt->value.floating_point = v;
LOG_DEBUG("card10.cfg", "setting '%s' to %f", opt->name, v);
return true;
}
int v = strtol(value, &endptr, 0);
const char *elide(const char *str)
{
static char ret[21];
size_t len = strlen(str);
if (len <= 20) {
return str;
if (endptr != (value + len)) {
return NOT_INT_MAGIC;
}
strncpy(ret, str, 17);
ret[17] = '.';
ret[18] = '.';
ret[19] = '.';
ret[20] = '\0';
return ret;
}
static bool set_string(struct config_option *opt, const char *value)
{
//this leaks, but the lifetime of these ends when epicardium exits, so...
char *leaks = strdup(value);
opt->value.string = leaks;
LOG_DEBUG("card10.cfg", "setting '%s' to %s", opt->name, elide(leaks));
return true;
return v;
}
static void configure(const char *key, const char *value, int lineNumber)
{
struct config_option *opt = findOption(key);
if (!opt) {
//invalid key
// 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: ignoring unknown option '%s'",
lineNumber,
key
"line:%d: too long - aborting",
line_number
);
return;
}
bool ok = false;
switch (opt->type) {
case OptionType_Boolean:
ok = set_bool(opt, value);
break;
case OptionType_Int:
ok = set_int(opt, value);
break;
case OptionType_Float:
ok = set_float(opt, value);
break;
case OptionType_String:
ok = set_string(opt, value);
break;
default:
assert(0 && "unreachable");
}
if (!ok) {
LOG_WARN(
"card10.cfg",
"line %d: ignoring invalid value '%s' for option '%s'",
lineNumber,
value,
key
);
}
config_slot *slot = allocate_config_slot();
strncpy(slot->key, key, KEY_LENGTH);
slot->value = try_parse_int(value);
slot->value_offset = value_offset;
}
static void doline(char *line, char *eol, int lineNumber)
// 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;
......@@ -189,9 +146,8 @@ static void doline(char *line, char *eol, int lineNumber)
if (*key) {
LOG_WARN(
"card10.cfg",
"line %d (%s): syntax error",
lineNumber,
elide(line)
"line %d: syntax error",
line_number
);
}
return;
......@@ -203,7 +159,7 @@ static void doline(char *line, char *eol, int lineNumber)
--e_key;
e_key[1] = '\0';
if (*key == '\0') {
LOG_WARN("card10.cfg", "line %d: empty key", lineNumber);
LOG_WARN("card10.cfg", "line %d: empty key", line_number);
return;
}
......@@ -220,43 +176,30 @@ static void doline(char *line, char *eol, int lineNumber)
LOG_WARN(
"card10.cfg",
"line %d: empty value for option '%s'",
lineNumber,
line_number,
key
);
return;
}
configure(key, value, lineNumber);
}
bool config_get_boolean(enum EpicConfigOption option)
{
struct config_option *opt = &s_options[option];
assert(opt->type == OptionType_Boolean);
return opt->value.boolean;
}
size_t value_offset = value - line_start + line_offset;
long config_get_integer(enum EpicConfigOption option)
{
struct config_option *opt = &s_options[option];
assert(opt->type == OptionType_Int);
return opt->value.integer;
}
double config_get_float(enum EpicConfigOption option)
{
struct config_option *opt = &s_options[option];
assert(opt->type == OptionType_Float);
return opt->value.floating_point;
add_config_pair(key, value, line_number, value_offset);
}
const char *config_get_string(enum EpicConfigOption option)
// 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)
{
struct config_option *opt = &s_options[option];
assert(opt->type == OptionType_String);
return opt->value.string;
while (n--) {
if (*buf == '\r') {
*buf = '\n';
}
buf++;
}
}
// parses the entire config file
void load_config(void)
{
LOG_DEBUG("card10.cfg", "loading...");
......@@ -270,12 +213,14 @@ void load_config(void)
);
return;
}
char buf[CONFIG_MAX_LINE_LENGTH + 1];
int lineNumber = 0;
char buf[MAX_LINE_LENGTH + 1];
int line_number = 0;
size_t file_offset = 0;
int nread;
do {
nread = epic_file_read(fd, buf, CONFIG_MAX_LINE_LENGTH);
if (nread < CONFIG_MAX_LINE_LENGTH) {
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';
}
......@@ -285,64 +230,184 @@ void load_config(void)
char *eol = NULL;
int last_eol = 0;
while (line) {
//line points one character past the las (if any) '\n' hence '- 1'
//line points one character past the last (if any) '\n' hence '- 1'
last_eol = line - buf - 1;
eol = strchr(line, '\n');
++lineNumber;
++line_number;
if (eol) {
*eol = '\0';
doline(line, eol, lineNumber);
parse_line(line, eol, line_number, file_offset);
file_offset += eol - line + 1;
line = eol + 1;
} else {
if (line == buf) {
//line did not fit into buf
LOG_WARN(
"card10.cfg",
"line:%d: too long - aborting",
lineNumber
);
return;
} else {
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) {
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') {
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;
}
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 == sizeof(buf));
} 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(