Skip to content
Snippets Groups Projects
Select Git revision
  • ebd3d0168270c11988a38a4be0f9066a0149f20d
  • master default protected
  • right_scroll_config
  • ofscreen_lines
  • simple_menu_buttons
  • rahix/meson-micropython
  • schneider/ecg-plot
  • rahix/proper-sleep
  • rahix/panic
  • genofire/ble-follow-py
  • schneider/ble-stability-new-phy-adv
  • schneider/ble-stability
  • schneider/ble-stability-new-phy
  • add_menu_vibration
  • plaetzchen/ios-workaround
  • blinkisync-as-preload
  • schneider/max30001-pycardium
  • schneider/max30001-epicaridum
  • schneider/max30001
  • schneider/stream-locks
  • schneider/fundamental-test
  • v1.11
  • v1.10
  • v1.9
  • v1.8
  • v1.7
  • v1.6
  • v1.5
  • v1.4
  • v1.3
  • v1.2
  • v1.1
  • v1.0
  • release-1
  • bootloader-v1
  • v0.0
36 results

config.c

Blame
  • Forked from card10 / firmware
    Source project has a limited visibility.
    config.c 12.75 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 <strings.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <stddef.h>
    #include <stdio.h>
    
    #define MAX_LINE_LENGTH 80
    #define KEYS_PER_BLOCK 16
    #define KEY_LENGTH 16
    #define NOT_INT_MAGIC ((int)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 = &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;
    }
    
    // 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 = &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;
    	}
    }
    
    // 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;
    }
    
    static void trim(char *str)
    {
    	char *start = str;
    	while (*start && !isgraph(*start))
    		start++;
    
    	if (strlen(start) > 0) {
    		char *end = start + strlen(start) - 1;
    		while (*end && !isgraph(*end))
    			end--;
    		end[1] = 0;
    	}
    
    	memmove(str, start, strlen(start) + 1);
    }
    
    // parses one line of the config file
    static void parse_line(char *line, int line_number, size_t line_offset)
    {
    	char *line_start  = line;
    	char *value_start = strchr(line, '=') + 1;
    
    	trim(line);
    
    	//printf(line);
    	if (*line == '#') {
    		//skip comments
    		return;
    	}
    
    	char *eq = strchr(line, '=');
    	if (!eq) {
    		if (*line) {
    			LOG_WARN(
    				"card10.cfg",
    				"line %d: syntax error",
    				line_number
    			);
    		}
    		return;
    	}
    	*eq = 0;
    
    	char *key = line;
    	trim(key);
    
    	if (*key == '\0') {
    		LOG_WARN("card10.cfg", "line %d: empty key", line_number);
    		return;
    	}
    
    	char *value = eq + 1;
    	trim(value);
    
    	if (*value == '\0') {
    		LOG_WARN(
    			"card10.cfg",
    			"line %d: empty value for option '%s'",
    			line_number,
    			key
    		);
    		return;
    	}
    
    	size_t value_offset = value_start - line_start + line_offset;
    
    	add_config_pair(key, value, line_number, value_offset);
    }
    
    typedef struct {
    	int line_number;
    	int file_offset;
    	int line_start;
    	char line[MAX_LINE_LENGTH + 1];
    	int line_length;
    } parser_state;
    
    int parse_character(char c, parser_state *s)
    {
    	if (c != '\r' && c != '\n') {
    		if (s->line_length == MAX_LINE_LENGTH) {
    			LOG_WARN(
    				"card10.cfg",
    				"line:%d: too long - aborting",
    				s->line_number
    			);
    			return -1;
    		}
    		s->line[s->line_length++] = c;
    	} else {
    		s->line[s->line_length] = 0;
    		//printf("New line: %s (%d %d)\n", s->line, s->line_number, s->line_start);
    		parse_line(s->line, s->line_number, s->line_start);
    		s->line_length = 0;
    		s->line_start  = s->file_offset + 1;
    		if (c == '\n') {
    			s->line_number++;
    		}
    	}
    
    	s->file_offset++;
    	return 0;
    }
    
    // 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[128];
    	int nread;
    	parser_state s;
    	memset(&s, 0, sizeof(s));
    	s.line_number = 1;
    	do {
    		nread = epic_file_read(fd, buf, sizeof(buf));
    		int i;
    		for (i = 0; i < nread; i++) {
    			parse_character(buf[i], &s);
    		}
    	} while (nread == sizeof(buf));
    	parse_character('\n', &s);
    
    	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", "seek2 failed (%d), aborting", rc);
    		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 *end = buf;
    	while (*end && !iscntrl(*end))
    		end++;
    	*end = 0;
    	trim(buf);
    
    	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 (!strcasecmp(buf, "true")) {
    		*value = true;
    		return 0;
    	} else if (!strcasecmp(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;
    	}
    }
    
    int epic_config_set_string(const char *key, const char *value_in)
    {
    	char value[MAX_LINE_LENGTH + 1];
    
    	if (strlen(key) > MAX_LINE_LENGTH) {
    		return -EINVAL;
    	}
    
    	/* TODO: Change interface of trim to take the buffer and size directly */
    	if (strlen(value_in) > MAX_LINE_LENGTH) {
    		return -EINVAL;
    	}
    
    	strcpy(value, value_in);
    	trim(value);
    
    	if (snprintf(NULL, 0, "\n%s = %s\n", key, value) > MAX_LINE_LENGTH) {
    		return -EINVAL;
    	}
    
    	/* Check if key is sane. No control characters, spaces, equal signs or pounds allowed */
    	for (size_t i = 0; i < strlen(key); i++) {
    		char c = key[i];
    		if (!isgraph(c) || c == '=' || c == '#') {
    			return -EINVAL;
    		}
    	}
    	/* Check if value is sane. No control characters allowed */
    	for (size_t i = 0; i < strlen(value); i++) {
    		char c = value[i];
    		if (!isprint(c)) {
    			return -EINVAL;
    		}
    	}
    
    	config_slot *slot = find_config_slot(key);
    	bool present      = slot && slot->value_offset;
    	int ret           = 0;
    
    	if (!present) {
    		/* Easy case: We simply add the new option at the
    		 * end of the file. */
    
    		char buf[MAX_LINE_LENGTH];
    		/* Leading new line because I'm lazy */
    		ret = snprintf(buf, sizeof(buf), "\n%s = %s\n", key, value);
    
    		if (ret < 0 || ret >= (int)sizeof(buf)) {
    			return -EINVAL;
    		}
    
    		int fd = epic_file_open("card10.cfg", "a");
    		if (fd < 0) {
    			LOG_DEBUG(
    				"card10.cfg",
    				"open for appending failed: %s (%d)",
    				strerror(-fd),
    				fd
    			);
    			return fd;
    		}
    
    		int write_ret = epic_file_write(fd, buf, strlen(buf));
    		if (write_ret < 0) {
    			LOG_DEBUG(
    				"card10.cfg",
    				"writing failed: %s (%d)",
    				strerror(-write_ret),
    				write_ret
    			);
    		}
    
    		if (write_ret < (int)strlen(buf)) {
    			LOG_DEBUG(
    				"card10.cfg",
    				"writing failed to write all bytes (%d of %d)",
    				write_ret,
    				strlen(buf)
    			);
    		}
    
    		ret = epic_file_close(fd);
    
    		if (ret < 0) {
    			LOG_DEBUG(
    				"card10.cfg",
    				"close failed: %s (%d)",
    				strerror(-ret),
    				ret
    			);
    		}
    		if (write_ret < 0) {
    			ret = write_ret;
    		}
    		if (ret < 0) {
    			goto out;
    		}
    		if (write_ret < (int)strlen(buf)) {
    			LOG_DEBUG(
    				"card10.cfg",
    				"writing failed to write all bytes (%d of %d)",
    				write_ret,
    				strlen(buf)
    			);
    			ret = -EIO;
    			goto out;
    		}
    	} else {
    		/* Complex case: The value is already somewhere in the file.
    		 * We do not want to lose existing formatting or comments.
    		 * Solution: Copy parts of the file, insert new value, copy
    		 * rest, rename.
    		 */
    		char buf[MAX_LINE_LENGTH + 1];
    		int fd1 = -1;
    		int fd2 = -1;
    		ret     = epic_config_get_string(key, buf, sizeof(buf));
    
    		if (ret == 0 && strcmp(buf, value) == 0) {
    			/* Nothing to do: the values are the same. */
    			return 0;
    		}
    
    		size_t nread = read_config_offset(
    			slot->value_offset, buf, sizeof(buf)
    		);
    		if (nread == 0) {
    			LOG_DEBUG("card10.cfg", "could not read old value");
    			ret = -EIO;
    			goto complex_out;
    		}
    
    		char *end = buf;
    		while (*end && (!iscntrl(*end) || isblank(*end)))
    			end++;
    		*end = 0;
    
    		int old_len = strlen(buf);
    
    		fd1 = epic_file_open("card10.cfg", "r");
    		if (fd1 < 0) {
    			LOG_DEBUG(
    				"card10.cfg",
    				"open for read failed: %s (%d)",
    				strerror(-fd1),
    				fd1
    			);
    			ret = fd1;
    			goto complex_out;
    		}
    
    		fd2 = epic_file_open("card10.nfg", "w");
    		if (fd2 < 0) {
    			LOG_DEBUG(
    				"card10.nfg",
    				"open for writing failed: %s (%d)",
    				strerror(-fd2),
    				fd2
    			);
    			ret = fd2;
    			goto complex_out;
    		}
    
    		/* Copy over slot->value_offset bytes */
    		int i = slot->value_offset;
    		while (i > 0) {
    			int n = i > (int)sizeof(buf) ? (int)sizeof(buf) : i;
    			ret   = epic_file_read(fd1, buf, n);
    			if (ret < 0) {
    				LOG_DEBUG(
    					"card10.cfg",
    					"read failed: rc: %d",
    					ret
    				);
    				goto complex_out;
    			}
    
    			int ret2 = epic_file_write(fd2, buf, ret);
    
    			if (ret2 < 0) {
    				ret = ret2;
    				LOG_DEBUG(
    					"card10.nfg",
    					"write failed: rc: %d",
    					ret
    				);
    				goto complex_out;
    			}
    			i -= ret;
    		}
    
    		/* Insert new value into the new file */
    		ret = epic_file_write(fd2, value, strlen(value));
    		if (ret < 0) {
    			LOG_DEBUG("card10.nfg", "write failed: rc: %d", ret);
    			goto complex_out;
    		}
    
    		/* Skip the old value inside the old file */
    		epic_file_seek(fd1, old_len, SEEK_CUR);
    
    		/* Copy the rest of the old file to the new file */
    		while (true) {
    			int ret = epic_file_read(fd1, buf, sizeof(buf));
    
    			if (ret == 0) {
    				break;
    			}
    
    			if (ret < 0) {
    				LOG_DEBUG(
    					"card10.cfg",
    					"read failed: rc: %d",
    					ret
    				);
    				goto complex_out;
    			}
    
    			int ret2 = epic_file_write(fd2, buf, ret);
    
    			if (ret2 < 0) {
    				ret = ret2;
    				LOG_DEBUG(
    					"card10.nfg",
    					"write failed: rc: %d",
    					ret
    				);
    				goto complex_out;
    			}
    
    			if (ret < (int)sizeof(buf)) {
    				break;
    			}
    		}
    
    	complex_out:
    		if (fd1 >= 0) {
    			epic_file_close(fd1);
    		}
    		if (fd2 >= 0) {
    			int ret2 = epic_file_close(fd2);
    			if (ret >= 0) {
    				ret = ret2;
    			}
    		}
    
    		if (ret >= 0) {
    			epic_file_unlink("card10.cfg");
    			epic_file_rename("card10.nfg", "card10.cfg");
    		}
    	}
    
    out:
    	/* Reload config so the new key or the changed value is available */
    	load_config();
    
    	return ret < 0 ? ret : 0;
    }