tio/src/configfile.c
Maximilian Seesslen 7e61a34df3 Added timestamp format "epoch-usec"
This timestamp format will print the seconds since epoch along with
subdivision in microseconds.

Example:
 [1748009585.087083] tio v3.9-8-g2fb788f-dirty
 [1748009585.087156] Press ctrl-t q to quit
 [1748009585.087683] Connected to /dev/ttyUSB0
2025-05-23 16:36:39 +02:00

788 lines
22 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 <stdbool.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <libgen.h>
#include <errno.h>
#include <regex.h>
#include <glib.h>
#include "configfile.h"
#include "timestamp.h"
#include "print.h"
#include "rs485.h"
#include "misc.h"
#define CONFIG_GROUP_NAME_DEFAULT "default"
#define CONFIG_GROUP_INCLUDE_PREFIX "include "
#define MAX_LINE_LENGTH 1024
struct config_t config = {};
static void config_file_load(const char *filename, GString *buffer, bool test);
static void config_file_process(const char *filename, GString *buffer, GList **included_files, bool test);
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;
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.local_echo);
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", (bool*) &option.timestamp);
if (option.timestamp != TIMESTAMP_NONE)
{
config_get_string(key_file, group, "timestamp-format", &string, "24hour", "24hour-start", "24hour-delta", "iso8601", "epoch", "epoch-usec", 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_string(key_file, group, "log-directory", &option.log_directory, 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", &string, NULL);
if (string != NULL)
{
option_parse_mappings(string);
g_free((void *)string);
string = 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, "exec", &option.exec, 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)
{
GString *config_buffer;
GError *error = NULL;
GKeyFile *keyfile;
// Reset configuration
memset(&config, 0, sizeof(struct config_t));
// Find config file
if (config_file_resolve() != 0)
{
// None found - stop parsing
return;
}
// Load content of configuration file into buffer
config_buffer = g_string_new(NULL);
config_file_load(config.path, config_buffer, false);
// Load configuration
keyfile = g_key_file_new();
if (g_key_file_load_from_data(keyfile, config_buffer->str, config_buffer->len, G_KEY_FILE_NONE, &error) == false)
{
g_error_free(error);
goto error_load;
}
// 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;
}
// Skip group with include directive
if (strncmp(group[i], CONFIG_GROUP_INCLUDE_PREFIX, strlen(CONFIG_GROUP_INCLUDE_PREFIX)) == 0)
{
continue;
}
printf("%s ", group[i]);
}
g_strfreev(group);
error_load:
g_key_file_free(keyfile);
g_string_free(config_buffer, TRUE);
}
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[14] = {};
regex_t regex;
assert(str != NULL);
assert(pattern != NULL);
assert(device != NULL);
char *string = calloc(PATH_MAX, 1);
if (string == NULL)
{
tio_debug_printf("Failure allocating string memory\n");
return NULL;
}
strncpy(string, device, PATH_MAX - 1);
/* 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
tio_error_print("Failure compiling regular expression '%s'\n", pattern);
exit(EXIT_FAILURE);
}
regmatch_t matches[regex.re_nsub + 1];
int status = regexec(&regex, str, regex.re_nsub + 1, matches, 0);
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 l = matches[i].rm_so; l < matches[i].rm_eo; l++)
{
tio_debug_printf_raw("%c", str[l]);
replacement_str[k++] = str[l];
}
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)
{
tio_debug_printf("No regex match\n");
goto error;
}
else
{
char error_message[100];
regerror(status, &regex, error_message, sizeof(error_message));
tio_debug_printf("Regex match failed: %s", error_message);
goto error;
}
regfree(&regex);
return string;
error:
regfree(&regex);
return NULL;
}
static void config_file_process_line(const char *line, GString *buffer, GList **included_files, bool test)
{
if (strncmp(line, "[include ", 9) == 0 && line[strlen(line) - 2] == ']')
{
char include_filename[MAX_LINE_LENGTH];
// Construct the format string safely
char format_string[50];
snprintf(format_string, sizeof(format_string), "[include %%%ds]", MAX_LINE_LENGTH - 1);
int ret = sscanf(line, format_string, include_filename);
if (ret != 1)
{
return;
}
// Remove the trailing ']' character
include_filename[strlen(include_filename) - 1] = '\0';
if (g_list_find_custom(*included_files, include_filename, (GCompareFunc)strcmp) != NULL)
{
// Already included, avoid recursion
return;
}
// Add to included files list
*included_files = g_list_append(*included_files, g_strdup(include_filename));
// Process the included file
config_file_process(include_filename, buffer, included_files, test);
}
else
{
// Normal line, add to buffer
g_string_append(buffer, line);
}
}
static void config_file_process(const char *filename, GString *buffer, GList **included_files, bool test)
{
if (test)
{
// Test that configuration file can be parsed
GError *error = NULL;
GKeyFile *keyfile = g_key_file_new();
if (g_key_file_load_from_file(keyfile, filename, G_KEY_FILE_NONE, &error) == false)
{
tio_error_print("Failure loading file %s: %s", filename, error->message);
g_key_file_free(keyfile);
g_error_free(error);
exit(EXIT_FAILURE);
}
}
FILE *file = fopen(filename, "r");
if (file)
{
char line[MAX_LINE_LENGTH];
while (fgets(line, sizeof(line), file))
{
config_file_process_line(line, buffer, included_files, test);
}
fclose(file);
}
}
static void config_file_load(const char *filename, GString *buffer, bool test)
{
char current_dir[PATH_MAX] = ".";
char *config_file_dir = dirname(strdup(config.path));
GList *included_files = NULL;
getcwd(current_dir, PATH_MAX);
// Change to the directory of the configuration file
chdir(config_file_dir);
config_file_process(filename, buffer, &included_files, test);
// Restore current directory
chdir(current_dir);
// Free memory
g_list_free_full(included_files, g_free);
free(config_file_dir);
}
void config_file_parse(void)
{
// Find config file
if (config_file_resolve() != 0)
{
// None found - stop parsing
return;
}
if (option.target == NULL)
{
return;
}
GString *config_buffer = g_string_new(NULL);
GKeyFile *keyfile = g_key_file_new();
GList *included_files = NULL;
GError *error = NULL;
config_file_load(config.path, config_buffer, true);
if (g_key_file_load_from_data(keyfile, config_buffer->str, config_buffer->len, G_KEY_FILE_NONE, &error) == false)
{
tio_error_print("Failure loading file %s: %s", config.path, error->message);
g_string_free(config_buffer, TRUE);
g_key_file_free(keyfile);
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
gchar *pattern = g_key_file_get_string(keyfile, group[i], "pattern", &error);
if (error != NULL)
{
g_error_free(error);
error = NULL;
continue;
}
// Lookup 'device' key
gchar *device = g_key_file_get_string(keyfile, group[i], "device", &error);
if (error != NULL)
{
g_error_free(error);
error = NULL;
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
device = strdup(config.device);
// Parse found group (may replace config.device)
config_parse_keys(keyfile, group[i]);
// Update configuration
config.active_group = strdup(group[i]);
config.device = device; // Restore new device string
break;
}
}
g_strfreev(group);
}
// Cleanup
g_key_file_free(keyfile);
g_string_free(config_buffer, TRUE);
g_list_free_full(included_files, g_free);
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);
}
}
}
void config_list_targets(void)
{
memset(&config, 0, sizeof(struct config_t));
// Find config file
if (config_file_resolve() != 0)
{
// None found
return;
}
GKeyFile *keyfile;
GError *error = NULL;
keyfile = g_key_file_new();
GString *config_buffer = g_string_new(NULL);
config_file_load(config.path, config_buffer, false);
if (g_key_file_load_from_data(keyfile, config_buffer->str, config_buffer->len, G_KEY_FILE_NONE, &error) == false)
{
g_error_free(error);
goto cleanup;
}
// Get all group names
gsize num_groups;
gchar **group = g_key_file_get_groups(keyfile, &num_groups);
if (num_groups == 0)
{
goto cleanup;
}
printf("\nConfiguration profiles (%s)\n", config.path);
printf("--------------------------------------------------------------------------------\n");
int j = 1;
for (gsize i = 0; i < num_groups; i++)
{
// Skip default group
if (strcmp(group[i], CONFIG_GROUP_NAME_DEFAULT) == 0)
{
continue;
}
// Skip group with include directive
if (strncmp(group[i], CONFIG_GROUP_INCLUDE_PREFIX, strlen(CONFIG_GROUP_INCLUDE_PREFIX)) == 0)
{
continue;
}
printf("%-19s ", group[i]);
if (j++ % 4 == 0)
{
putchar('\n');
}
}
if ((j-1) % 4 != 0)
{
putchar('\n');
}
g_strfreev(group);
cleanup:
g_key_file_free(keyfile);
g_string_free(config_buffer, TRUE);
}