tio/src/configfile.c
Martin Lund 65c5a068d8 Replace inih with glib key file parser
After including the use of glib we might as well replace inih
with the glib key file parser.

All configuraiton file parsing has been reworked and also the options
parsing has been cleaned up, resulting in better and stricter
configuration file and option value checks.

Compared to old, configuration files now requires any default
configurations to be put in a group/section named [default].

Configuration file keywords such as "enable", "disable", "on",
"off", "yes", "no", "0", "1" have been retired. Now only "true" and
"false" apply to boolean configuration options. This is done to simplify
things and avoid any confusion.

The pattern option feature has been reworked so now the user can now
access the full match string and any matching subexpression using the
%mN syntax.

For example:

[usb devices]
pattern = usb([0-9]*)
device = /dev/ttyUSB%m1

Then when using tio:
$ tio usb12

   %m0 = 'usb12'  // Full match string
   %m1 = 12       // First match subexpression

Which results in device = /dev/ttyUSB12
2024-05-02 18:35:37 +02:00

601 lines
17 KiB
C

/*
* tio - a serial device I/O tool
*
* Copyright (c) 2020-2022 Liam Beguin
* Copyright (c) 2022 Martin Lund
*
* This program 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 2
* of the License, or (at your option) any later version.
*
* This program 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
#define _GNU_SOURCE
#include "config.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include <errno.h>
#include <getopt.h>
#include <termios.h>
#include <assert.h>
#include <limits.h>
#include <unistd.h>
#include <regex.h>
#include <glib.h>
#include "options.h"
#include "configfile.h"
#include "misc.h"
#include "options.h"
#include "error.h"
#include "print.h"
#include "rs485.h"
#include "timestamp.h"
#include "alert.h"
#define CONFIG_GROUP_NAME_DEFAULT "default"
struct config_t config = {};
static void config_get_string(GKeyFile *key_file, gchar *group, gchar *key, char **dest, char *allowed_string, ...)
{
(void)dest;
GError *error = NULL;
bool mismatch = true;
gchar *string = g_key_file_get_string(key_file, group, key, &error);
if (error != NULL)
{
if (error->code == G_KEY_FILE_ERROR_KEY_NOT_FOUND)
{
// Key not found - ignore key
g_error_free(error);
return;
}
tio_error_print("%s: %s", config.path, error->message);
g_error_free(error);
exit(EXIT_FAILURE);
}
va_list args;
const char* current_arg = allowed_string;
va_start(args, allowed_string);
if (current_arg == NULL)
{
mismatch = false;
}
// Iterate through variable arguments
while (current_arg != NULL)
{
if (strcmp(string, current_arg) == 0)
{
mismatch = false;
break;
}
current_arg = va_arg(args, const char *);
}
if (mismatch)
{
tio_error_print("%s: Invalid %s value '%s' in %s profile", config.path, key, string, group);
exit(EXIT_FAILURE);
}
va_end(args);
*dest = string;
}
static void config_get_integer(GKeyFile *key_file, gchar *group, gchar *key, int *dest, int min, int max)
{
(void)dest;
GError *error = NULL;
int value = g_key_file_get_integer(key_file, group, key, &error);
if (error != NULL)
{
if (error->code == G_KEY_FILE_ERROR_KEY_NOT_FOUND)
{
// Key not found - ignore key
g_error_free(error);
return;
}
tio_error_print("%s: %s", config.path, error->message);
g_error_free(error);
exit(EXIT_FAILURE);
}
if ((value < min) || (value > max))
{
tio_error_print("%s: Invalid %s value '%d' in %s profile", config.path, key, value, group);
exit(EXIT_FAILURE);
}
*dest = value;
}
static void config_get_bool(GKeyFile *key_file, gchar *group, gchar *key, bool *dest)
{
(void)dest;
GError *error = NULL;
bool value = g_key_file_get_boolean(key_file, group, key, &error);
if (error != NULL)
{
if (error->code == G_KEY_FILE_ERROR_KEY_NOT_FOUND)
{
// Key not found - ignore key
g_error_free(error);
return;
}
tio_error_print("%s: %s", config.path, error->message);
g_error_free(error);
exit(EXIT_FAILURE);
}
*dest = value;
}
static void config_parse_keys(GKeyFile *key_file, char *group)
{
char *string = NULL;
bool boolean = false;
config_get_string(key_file, group, "device", &config.device, NULL);
config_get_integer(key_file, group, "baudrate", &option.baudrate, 0, INT_MAX);
config_get_integer(key_file, group, "databits", &option.databits, 5, 8);
config_get_string(key_file, group, "flow", &string, "none", "hard", "soft", NULL);
if (string != NULL)
{
option_parse_flow(string, &option.flow);
g_free((void *)string);
string = NULL;
}
config_get_integer(key_file, group, "stopbits", &option.stopbits, 1, 2);
config_get_string(key_file, group, "parity", &string, "odd", "even", "none", "mark", "space", NULL);
if (string != NULL)
{
option_parse_parity(string, &option.parity);
g_free((void *)string);
string = NULL;
}
config_get_integer(key_file, group, "output-delay", &option.output_delay, 0, INT_MAX);
config_get_integer(key_file, group, "output-line-delay", &option.output_line_delay, 0, INT_MAX);
config_get_string(key_file, group, "line-pulse-duration", &string, NULL);
if (string != NULL)
{
option_parse_line_pulse_duration(string);
g_free((void *)string);
string = NULL;
}
config_get_string(key_file, group, "auto-connect", &string, "new", "latest", "direct", NULL);
if (string != NULL)
{
option_parse_auto_connect(string, &option.auto_connect);
g_free((void *)string);
string = NULL;
}
config_get_string(key_file, group, "exclude-devices", &option.exclude_devices, NULL);
config_get_string(key_file, group, "exclude-drivers", &option.exclude_devices, NULL);
config_get_string(key_file, group, "exclude-tids", &option.exclude_devices, NULL);
config_get_bool(key_file, group, "no-reconnect", &option.no_reconnect);
config_get_bool(key_file, group, "local-echo", &option.no_reconnect);
config_get_string(key_file, group, "input-mode", &string, "normal", "hex", "line", NULL);
if (string != NULL)
{
option_parse_input_mode(string, &option.input_mode);
g_free((void *)string);
string = NULL;
}
config_get_string(key_file, group, "output-mode", &string, NULL);
if (string != NULL)
{
option_parse_output_mode(string, &option.output_mode);
g_free((void *)string);
string = NULL;
}
config_get_bool(key_file, group, "timestamp", &boolean);
if (boolean == true)
{
option.timestamp = TIMESTAMP_24HOUR;
}
config_get_string(key_file, group, "timestamp-format", &string, "24hour", "24hour-start", "24hour-delta", "iso8601", NULL);
if (string != NULL)
{
option_parse_timestamp(string, &option.timestamp);
g_free((void *)string);
string = NULL;
}
config_get_integer(key_file, group, "timestamp-timeout", &option.timestamp_timeout, 0, INT_MAX);
config_get_bool(key_file, group, "log", &option.log);
config_get_string(key_file, group, "log-file", &option.log_filename, NULL);
config_get_bool(key_file, group, "log-append", &option.log_append);
config_get_bool(key_file, group, "log-strip", &option.log_strip);
config_get_string(key_file, group, "map", &option.map, NULL);
config_get_string(key_file, group, "color", &string, NULL);
if (string != NULL)
{
if (strcmp(string, "list") == 0)
{
// Ignore
}
else if (strcmp(string, "none") == 0)
{
option.color = -1; // No color
}
else if (strcmp(string, "bold") == 0)
{
option.color = 256; // Bold
}
else
{
option.color = atoi(string);
if ((option.color < 0) || (option.color > 255))
{
tio_error_print("%s: Invalid color value in %s profile", config.path, group);
exit(EXIT_FAILURE);
}
}
g_free((void *)string);
string = NULL;
}
config_get_string(key_file, group, "socket", &option.socket, NULL);
config_get_bool(key_file, group, "rs-485", &option.rs485);
config_get_string(key_file, group, "rs-385-config", &string, NULL);
if (string != NULL)
{
rs485_parse_config(string);
g_free((void *)string);
string = NULL;
}
config_get_string(key_file, group, "alert", &string, "bell", "blink", "none", NULL);
if (string != NULL)
{
option_parse_alert(string, &option.alert);
g_free((void *)string);
string = NULL;
}
config_get_bool(key_file, group, "mute", &option.mute);
config_get_string(key_file, group, "script", &option.script, NULL);
config_get_string(key_file, group, "script-file", &option.script_filename, NULL);
config_get_string(key_file, group, "script-run", &string, NULL);
if (string != NULL)
{
option_parse_script_run(string, &option.script_run);
g_free((void *)string);
string = NULL;
}
config_get_string(key_file, group, "prefix-ctrl-key", &string, NULL);
if (string != NULL)
{
if (strcmp(string, "none") == 0)
{
option.prefix_enabled = false;
}
else if (strlen(string) >= 2)
{
tio_error_print("%s: Invalid prefix-ctrl-key value in %s profile", config.path, group);
exit(EXIT_FAILURE);
}
else if (ctrl_key_code(string[0]) > 0)
{
option.prefix_enabled = true;
option.prefix_code = ctrl_key_code(string[0]);
option.prefix_key = string[0];
}
else
{
tio_error_print("%s: Invalid prefix-ctrl-key value in %s profile", config.path, group);
exit(EXIT_FAILURE);
}
g_free((void *)string);
string = NULL;
}
}
static int config_file_resolve(void)
{
char *xdg = getenv("XDG_CONFIG_HOME");
if (xdg)
{
if (asprintf(&config.path, "%s/tio/config", xdg) != -1)
{
if (access(config.path, F_OK) == 0)
{
return 0;
}
free(config.path);
}
}
char *home = getenv("HOME");
if (home)
{
if (asprintf(&config.path, "%s/.config/tio/config", home) != -1)
{
if (access(config.path, F_OK) == 0)
{
return 0;
}
free(config.path);
}
if (asprintf(&config.path, "%s/.tioconfig", home) != -1)
{
if (access(config.path, F_OK) == 0)
{
return 0;
}
free(config.path);
}
}
config.path = NULL;
return -EINVAL;
}
void config_file_show_profiles(void)
{
memset(&config, 0, sizeof(struct config_t));
// Find config file
if (config_file_resolve() != 0)
{
// None found - stop parsing
return;
}
GKeyFile *keyfile;
GError *error = NULL;
keyfile = g_key_file_new();
if (!g_key_file_load_from_file(keyfile, config.path, G_KEY_FILE_NONE, &error))
{
tio_error_print("Failure loading file: %s", error->message);
g_error_free(error);
return;
}
// Get all group names
gsize num_groups;
gchar **group = g_key_file_get_groups(keyfile, &num_groups);
for (gsize i = 0; i < num_groups; i++)
{
// Skip default group
if (strcmp(group[i], CONFIG_GROUP_NAME_DEFAULT) == 0)
{
continue;
}
}
g_strfreev(group);
g_key_file_free(keyfile);
}
static void replace_substring(char *str, const char *substr, const char *replacement)
{
char *pos = strstr(str, substr);
if (pos != NULL)
{
int substrLen = strlen(substr);
int replacementLen = strlen(replacement);
memmove(pos + replacementLen, pos + substrLen, strlen(pos + substrLen) + 1);
memcpy(pos, replacement, replacementLen);
}
}
static char *match_and_replace(const char *str, const char *pattern, char *device)
{
char replacement_str[PATH_MAX] = {};
char m_key[13] = {};
regex_t regex;
assert(str != NULL);
assert(pattern != NULL);
assert(device != NULL);
char *string = malloc(strlen(device) + PATH_MAX);
if (string == NULL)
{
return NULL;
}
strcpy(string, device);
/* Find matches of pattern in str. For each match, replace any '%mN' in the
* copy of the device string with the corresponding match subexpression and
* return the new formed device string.
*
* Note: %m0 = Full match expression.
* %m1 = First subexpression
* %m2 = Second subexpression
* %m3 = etc..
*/
if (regcomp(&regex, pattern, REG_EXTENDED) != 0)
{
// Failure to compile regular expression
return NULL;
}
regmatch_t matches[regex.re_nsub + 1];
int status = regexec(&regex, str, regex.re_nsub + 1, matches, REG_EXTENDED);
if (status == 0)
{
tio_debug_printf("Full match: ");
int j = 0;
for (int i = matches[0].rm_so; i < matches[0].rm_eo; i++)
{
tio_debug_printf_raw("%c", str[i]);
replacement_str[j++] = str[i];
}
replacement_str[j] = '\0';
replace_substring(string, "%m0", replacement_str);
tio_debug_printf_raw("\n");
for (int i = 1; i < ((int)regex.re_nsub + 1) && matches[i].rm_so != -1; i++)
{
tio_debug_printf("Subexpression %d match: ", i);
int k = 0;
for (int j = matches[i].rm_so; j < matches[i].rm_eo; j++)
{
tio_debug_printf_raw("%c", str[j]);
replacement_str[k++] = str[j];
}
replacement_str[k] = '\0';
sprintf(m_key, "%%m%d", i);
replace_substring(string, m_key, replacement_str);
tio_debug_printf_raw("\n");
}
}
else if (status == REG_NOMATCH)
{
goto error;
}
else
{
char error_message[100];
regerror(status, &regex, error_message, sizeof(error_message));
tio_error_print("Regex match failed: %s", error_message);
goto error;
}
regfree(&regex);
return string;
error:
regfree(&regex);
return NULL;
}
void config_file_parse(void)
{
// Find config file
if (config_file_resolve() != 0)
{
// None found - stop parsing
return;
}
if (option.target == NULL)
{
return;
}
GKeyFile *keyfile = g_key_file_new();
GError *error = NULL;
if (g_key_file_load_from_file(keyfile, config.path, G_KEY_FILE_NONE, &error) == false)
{
tio_error_print("Failure loading file %s: %s", config.path, error->message);
g_error_free(error);
exit(EXIT_FAILURE);
}
// Parse default group/section
if (g_key_file_has_group(keyfile, CONFIG_GROUP_NAME_DEFAULT))
{
config_parse_keys(keyfile, CONFIG_GROUP_NAME_DEFAULT);
}
// Parse target
if (g_key_file_has_group(keyfile, option.target))
{
config.active_group = strdup(option.target);
config_parse_keys(keyfile, option.target);
}
else
{
// Find group by pattern
gsize num_groups;
gchar **group = g_key_file_get_groups(keyfile, &num_groups);
for (gsize i = 0; i < num_groups; i++)
{
// Skip default group
if (strcmp(group[i], CONFIG_GROUP_NAME_DEFAULT) == 0)
{
continue;
}
// Lookup 'pattern' key
GError *error = NULL;
gchar *pattern = g_key_file_get_string(keyfile, group[i], "pattern", &error);
if (error != NULL)
{
g_error_free(error);
continue;
}
// Lookup 'device' key
gchar *device = g_key_file_get_string(keyfile, group[i], "device", &error);
if (error != NULL)
{
g_error_free(error);
continue;
}
// Match pattern against target and replace any sub expression
// matches (%mN) in device string and return resulting string
// representing the new pattern based string.
config.device = match_and_replace(option.target, pattern, device);
if (config.device != NULL)
{
// Match found - save device
char *device = strdup(config.device);
// Parse found group
config_parse_keys(keyfile, group[i]);
// Update configuration
config.active_group = strdup(group[i]);
config.device = device;
break;
}
}
g_strfreev(group);
}
g_key_file_free(keyfile);
atexit(&config_exit);
}
void config_exit(void)
{
free(config.active_group);
free(config.path);
free(config.device);
}
void config_file_print(void)
{
if (config.path != NULL)
{
tio_printf(" Active configuration file: %s", config.path);
if (config.active_group != NULL)
{
tio_printf(" Active configuration profile: %s", config.active_group);
}
}
}