/* * 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 #include #include #include #include #include #include #include #include #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(®ex, 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(®ex, 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, ®ex, error_message, sizeof(error_message)); tio_debug_printf("Regex match failed: %s", error_message); goto error; } regfree(®ex); return string; error: regfree(®ex); 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); }