ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
Config Module

The toml text file configuration file module. More...

Files

file  config.c
 ðŸ“‹ TOML configuration file parser with schema validation and CLI override support
 
file  config.h
 TOML configuration file support for ascii-chat.
 

Functions

asciichat_error_t config_load_and_apply (asciichat_mode_t detected_mode, const char *config_path, bool strict, options_t *opts)
 Main function to load configuration from file and apply to global options.
 
asciichat_error_t config_load_system_and_user (asciichat_mode_t detected_mode, bool strict, options_t *opts)
 Load system config first, then user config (user config overrides system)
 
asciichat_error_t config_create_default (const char *config_path)
 Create default configuration file with all default values.
 

Detailed Description

The toml text file configuration file module.

This module provides functionality for loading configuration from TOML files (typically located at ~/.ascii-chat/config.toml). Configuration values are applied to global options, but command-line arguments always take precedence over config file values.

The interface provides:

Note
Configuration Priority: Command-line arguments override config file values. Config file values override default values. The config file is loaded before CLI argument parsing to ensure this precedence.
Configuration File Location: The config file is loaded from the ascii-chat configuration directory:
  • Unix: $XDG_CONFIG_HOME/ascii-chat/config.toml if set, otherwise ~/.ascii-chat/config.toml
  • Windows: APPDATA%\ascii-chat\config.toml if set, otherwise ~\.ascii-chat\config.toml
Error Handling: Config file parsing errors are non-fatal. If the file is missing, malformed, or contains invalid values, warnings are printed to stderr and the application continues with default values. Invalid individual values are skipped with warnings, but valid values are still applied.
Validation: All configuration values are validated using the same validation functions used by CLI argument parsing, ensuring consistency between config file and CLI option handling.
Warning
Password Storage: While passwords can be stored in the config file (via crypto.password), this is strongly discouraged for security reasons. A warning is printed if a password is found in the config file. Use CLI --password or environment variables instead.
File Permissions: Users should secure their config file to prevent unauthorized access, especially if it contains sensitive information like encryption keys or passwords.
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
October 2025

Function Documentation

◆ config_create_default()

asciichat_error_t config_create_default ( const char *  config_path)

#include <config.h>

Create default configuration file with all default values.

Parameters
config_pathPath to config file to create (NULL uses default location)
optsOptions structure with values to write (use defaults for a "default" config)
Returns
ASCIICHAT_OK on success, error code on failure

Creates a new configuration file at the specified path (or default location if config_path is NULL) with all configuration options set to values from opts.

The created file includes:

  • Version comment at the top (current ascii-chat version)
  • All supported configuration sections with default values
  • Comments explaining each option
Note
The function will create the directory structure if needed.
If the file already exists, it will not be overwritten (returns error).

Definition at line 1254 of file config.c.

1254 {
1255 char *config_path_expanded = NULL;
1256
1257 defer(SAFE_FREE(config_path_expanded));
1258
1259 // Create fresh options with all OPT_*_DEFAULT values
1260 options_t defaults = options_t_new();
1261
1262 // Allocate buffer for building config content (256KB should be plenty)
1263 const size_t BUFFER_CAPACITY = 256 * 1024;
1264 config_builder_t builder = {0};
1265 builder.buffer = SAFE_MALLOC(BUFFER_CAPACITY, char *);
1266 if (!builder.buffer) {
1267 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate config buffer");
1268 }
1269 defer(SAFE_FREE(builder.buffer));
1270 builder.capacity = BUFFER_CAPACITY;
1271
1272 // Build version comment in buffer
1273 if (!config_builder_append(&builder, "# ascii-chat configuration file\n")) {
1274 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1275 }
1276 if (!config_builder_append(&builder, "# Generated by ascii-chat v%d.%d.%d-%s\n", ASCII_CHAT_VERSION_MAJOR,
1278 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1279 }
1280 if (!config_builder_append(&builder, "#\n")) {
1281 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1282 }
1283 if (!config_builder_append(&builder, "# All options below are commented out because some configuration options\n")) {
1284 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1285 }
1286 if (!config_builder_append(&builder, "# conflict with each other (e.g., --file vs --url, --loop vs --url).\n")) {
1287 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1288 }
1289 if (!config_builder_append(&builder, "# Uncomment only the options you need and avoid conflicting combinations.\n")) {
1290 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1291 }
1292 if (!config_builder_append(&builder, "#\n")) {
1293 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1294 }
1295 if (!config_builder_append(&builder,
1296 "# If you upgrade ascii-chat and this version comment changes, you may need to\n")) {
1297 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1298 }
1299 if (!config_builder_append(&builder, "# delete and regenerate this file with: ascii-chat --config-create\n")) {
1300 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1301 }
1302 if (!config_builder_append(&builder, "#\n\n")) {
1303 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1304 }
1305
1306 // Get all options from schema
1307 size_t metadata_count = 0;
1308 const config_option_metadata_t *metadata = config_schema_get_all(&metadata_count);
1309 {
1310 const options_config_t *unified_config = options_preset_unified(NULL, NULL);
1311 if (unified_config) {
1312 (void)config_schema_build_from_configs(&unified_config, 1);
1313 options_config_destroy(unified_config);
1314 metadata = config_schema_get_all(&metadata_count);
1315 }
1316 }
1317
1318 // Build list of unique categories in order of first appearance
1319 const char *categories[16] = {0}; // Max expected categories
1320 size_t category_count = 0;
1321
1322 for (size_t i = 0; i < metadata_count && category_count < 16; i++) {
1323 const char *category = metadata[i].category;
1324 if (!category) {
1325 continue;
1326 }
1327
1328 // Check if category already in list
1329 bool found = false;
1330 for (size_t j = 0; j < category_count; j++) {
1331 if (categories[j] && strcmp(categories[j], category) == 0) {
1332 found = true;
1333 break;
1334 }
1335 }
1336
1337 if (!found) {
1338 categories[category_count++] = category;
1339 }
1340 }
1341
1342 // Build each section dynamically from schema
1343 for (size_t cat_idx = 0; cat_idx < category_count; cat_idx++) {
1344 const char *category = categories[cat_idx];
1345 if (!category) {
1346 continue;
1347 }
1348
1349 // Get all options for this category
1350 size_t cat_option_count = 0;
1351 const config_option_metadata_t **cat_options = config_schema_get_by_category(category, &cat_option_count);
1352
1353 if (!cat_options || cat_option_count == 0) {
1354 continue;
1355 }
1356
1357 // Add section header
1358 if (!config_builder_append(&builder, "[%s]\n", category)) {
1359 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1360 }
1361
1362 // Track which options we've written (to avoid duplicates)
1363 bool written_flags[64] = {0}; // Max options per category
1364
1365 // Add each option in this category
1366 for (size_t opt_idx = 0; opt_idx < cat_option_count && opt_idx < 64; opt_idx++) {
1367 const config_option_metadata_t *meta = cat_options[opt_idx];
1368 if (!meta || !meta->toml_key) {
1369 continue;
1370 }
1371
1372 // Skip if already written (duplicate)
1373 if (written_flags[opt_idx]) {
1374 continue;
1375 }
1376
1377 // Skip if this is a duplicate of another option (check by TOML key, not field_offset)
1378 // Note: Multiple options can map to the same field (e.g., server_log_file, client_log_file)
1379 bool is_duplicate = false;
1380 for (size_t j = 0; j < opt_idx; j++) {
1381 if (cat_options[j] && cat_options[j]->toml_key && meta->toml_key &&
1382 strcmp(cat_options[j]->toml_key, meta->toml_key) == 0) {
1383 is_duplicate = true;
1384 break;
1385 }
1386 }
1387 if (is_duplicate) {
1388 continue;
1389 }
1390
1391 // Get field pointer from default options (or mode-specific default if available)
1392 const char *field_ptr = ((const char *)&defaults) + meta->field_offset;
1393
1394 // Buffer to hold mode-specific default value if needed
1395 char mode_default_buffer[OPTIONS_BUFF_SIZE] = {0};
1396 int mode_default_int = 0;
1397
1398 // If this option has a mode_default_getter, use it to get the correct default
1399 if (meta->mode_default_getter) {
1400 asciichat_mode_t mode = extract_mode_from_bitmask(meta->mode_bitmask);
1401 if (mode != MODE_INVALID) {
1402 const void *default_value = meta->mode_default_getter(mode);
1403 if (default_value) {
1404 // Copy the default value to our buffer based on type
1405 if (meta->type == OPTION_TYPE_STRING || meta->type == OPTION_TYPE_CALLBACK) {
1406 const char *str_value = (const char *)default_value;
1407 SAFE_STRNCPY(mode_default_buffer, str_value, sizeof(mode_default_buffer));
1408 field_ptr = mode_default_buffer;
1409 } else if (meta->type == OPTION_TYPE_INT) {
1410 mode_default_int = *(const int *)default_value;
1411 field_ptr = (const char *)&mode_default_int;
1412 }
1413 }
1414 }
1415 }
1416
1417 // Add description comment if available
1418 if (meta->description && strlen(meta->description) > 0) {
1419 if (!config_builder_append(&builder, "# %s\n", meta->description)) {
1420 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1421 }
1422 }
1423
1424 // Format and add the option value using handler (commented out to avoid conflicts)
1425 if (g_type_handlers[meta->type].format_output) {
1426 char formatted_value[BUFFER_SIZE_MEDIUM] = {0};
1427 g_type_handlers[meta->type].format_output(field_ptr, meta->field_size, meta, formatted_value,
1428 sizeof(formatted_value));
1429 const char *output_key = meta->toml_key;
1430 size_t category_len = strlen(category);
1431 if (strncmp(meta->toml_key, category, category_len) == 0 && meta->toml_key[category_len] == '.') {
1432 output_key = meta->toml_key + category_len + 1; // Strip "<category>."
1433 }
1434
1435 if (config_key_should_be_commented(meta->toml_key)) {
1436 if (!config_builder_append(&builder, "# %s = %s\n", output_key, formatted_value)) {
1437 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1438 }
1439 } else {
1440 if (!config_builder_append(&builder, "%s = %s\n", output_key, formatted_value)) {
1441 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1442 }
1443 }
1444 }
1445
1446 // Add blank line after each option
1447 if (!config_builder_append(&builder, "\n")) {
1448 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1449 }
1450
1451 written_flags[opt_idx] = true;
1452 }
1453
1454 // Add blank line between sections (but not after the last section)
1455 if (cat_idx < category_count - 1) {
1456 if (!config_builder_append(&builder, "\n")) {
1457 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1458 }
1459 }
1460 }
1461
1462 // Now write the buffer to either stdout or a file
1463 if (config_path && strlen(config_path) > 0) {
1464 // User provided a filepath - write to that file with overwrite prompt
1465
1466 // Expand and validate the path
1467 config_path_expanded = expand_path(config_path);
1468 if (!config_path_expanded) {
1469 config_path_expanded = platform_strdup(config_path);
1470 }
1471
1472 if (!config_path_expanded) {
1473 return SET_ERRNO(ERROR_CONFIG, "Failed to resolve config file path");
1474 }
1475
1476 char *validated_config_path = NULL;
1477 asciichat_error_t validate_result =
1478 path_validate_user_path(config_path_expanded, PATH_ROLE_CONFIG_FILE, &validated_config_path);
1479 if (validate_result != ASCIICHAT_OK) {
1480 SAFE_FREE(validated_config_path);
1481 SAFE_FREE(config_path_expanded);
1482 return validate_result;
1483 }
1484 // Free the old path before reassigning (defer will free the new one)
1485 if (config_path_expanded != validated_config_path) {
1486 SAFE_FREE(config_path_expanded);
1487 }
1488 config_path_expanded = validated_config_path;
1489
1490 // Check if file already exists
1491 struct stat st;
1492 if (stat(config_path_expanded, &st) == 0) {
1493 // File exists - ask user if they want to overwrite
1494 log_plain("Config file already exists: %s", config_path_expanded);
1495
1496 bool overwrite = platform_prompt_yes_no("Overwrite", false); // Default to No
1497 if (!overwrite) {
1498 log_plain("Config file creation cancelled.");
1499 return SET_ERRNO(ERROR_CONFIG, "User cancelled overwrite");
1500 }
1501
1502 log_plain("Overwriting existing config file...");
1503 }
1504
1505 // Create directory if needed
1506 char *dir_path = platform_strdup(config_path_expanded);
1507 if (!dir_path) {
1508 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate memory for directory path");
1509 }
1510 defer(SAFE_FREE(dir_path));
1511
1512 // Find the last path separator
1513 char *last_sep = strrchr(dir_path, PATH_DELIM);
1514
1515 if (last_sep) {
1516 *last_sep = '\0';
1517 // Create directory recursively
1519 if (mkdir_result != ASCIICHAT_OK) {
1520 return mkdir_result;
1521 }
1522 }
1523
1524 // Open file for writing
1525 FILE *output_file = platform_fopen("file_stream", config_path_expanded, "w");
1526 if (!output_file) {
1527 return SET_ERRNO_SYS(ERROR_CONFIG, "Failed to open config file for writing: %s", config_path_expanded);
1528 }
1529 defer(SAFE_FCLOSE(output_file));
1530
1531 // Write buffer to file
1532 size_t written = fwrite(builder.buffer, 1, builder.size, output_file);
1533 if (written != builder.size) {
1534 return SET_ERRNO_SYS(ERROR_CONFIG, "Failed to write config to file: %s", config_path_expanded);
1535 }
1536 } else {
1537 // No filepath provided - write buffer to stdout with automatic retry on transient errors
1538 (void)platform_write_all(STDOUT_FILENO, builder.buffer, builder.size);
1539 // Flush C stdio buffer and terminal to ensure piped output is written immediately
1540 (void)fflush(stdout);
1541 (void)terminal_flush(STDOUT_FILENO);
1542 }
1543
1544 return ASCIICHAT_OK;
1545}
void options_config_destroy(options_config_t *config)
Free options config.
Definition builder.c:631
@ OPTION_TYPE_INT
Integer value (–count 42)
Definition builder.h:163
@ OPTION_TYPE_STRING
String value (–name foo)
Definition builder.h:164
@ OPTION_TYPE_CALLBACK
Custom parser function.
Definition builder.h:166
#define BUFFER_SIZE_MEDIUM
Medium buffer size (512 bytes)
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define SAFE_FCLOSE(fp)
Definition common.h:386
#define defer(action)
Defer a cleanup action until function scope exit.
Definition defer.h:36
#define SET_ERRNO_SYS(code, context_msg,...)
Set error code with custom message and system error context, returning the error code.
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_CONFIG
Definition error_codes.h:57
#define log_plain(...)
Plain logging - writes to both log file and stderr without timestamps or log levels.
Definition log/log.h:618
asciichat_mode_t
Mode type for options parsing.
#define OPTIONS_BUFF_SIZE
Buffer size for option string values.
options_t options_t_new(void)
Initialize options by parsing command-line arguments.
@ MODE_INVALID
Invalid mode.
#define DIR_PERM_PRIVATE
Directory permission: Private (owner read/write/execute only)
Definition filesystem.h:199
bool platform_prompt_yes_no(const char *question, bool default_yes)
Prompt the user for a yes/no answer.
Definition util.c:83
size_t platform_write_all(int fd, const void *buf, size_t count)
Write all bytes to a file descriptor, handling partial writes.
Definition system.c:224
#define PATH_DELIM
Platform-specific path separator character.
Definition filesystem.h:112
asciichat_error_t platform_mkdir_recursive(const char *path, int mode)
Create directories recursively (mkdir -p equivalent)
char * platform_strdup(const char *s)
Duplicate string (strdup replacement)
asciichat_error_t terminal_flush(int fd)
Flush terminal output.
FILE * platform_fopen(const char *name, const char *filename, const char *mode)
Safe file open stream (fopen replacement)
asciichat_error_t path_validate_user_path(const char *input, path_role_t role, char **normalized_out)
Validate and canonicalize a user-supplied filesystem path.
Definition path.c:1020
char * expand_path(const char *path)
Expand path with tilde (~) support.
Definition path.c:505
@ PATH_ROLE_CONFIG_FILE
Definition path.h:317
options_config_t * options_preset_unified(const char *program_name, const char *description)
Build unified options config with ALL options (binary + all modes)
Definition presets.c:51
const config_option_metadata_t ** config_schema_get_by_category(const char *category, size_t *count)
Get all option metadata for a category.
Definition schema.c:401
const config_option_metadata_t * config_schema_get_all(size_t *count)
Get all option metadata.
Definition schema.c:434
asciichat_error_t config_schema_build_from_configs(const options_config_t **configs, size_t num_configs)
Build schema dynamically from options builder configs.
Definition schema.c:240
Helper structure for building config content in a buffer.
Definition config.c:1184
size_t capacity
Definition config.c:1187
Option metadata for config file parsing.
Definition schema.h:49
option_type_t type
Value type (from builder)
Definition schema.h:52
size_t field_size
Size of field in options_t.
Definition schema.h:56
option_mode_bitmask_t mode_bitmask
Which modes this option applies to.
Definition schema.h:65
const void *(* mode_default_getter)(asciichat_mode_t mode)
Get mode-specific default value (NULL if not mode-aware)
Definition schema.h:83
size_t field_offset
offsetof(options_t, field) - where to store value
Definition schema.h:55
const char * description
Description for docs/help generation.
Definition schema.h:68
const char * category
Category name (e.g., "network", "client", "audio")
Definition schema.h:54
const char * toml_key
TOML key path (e.g., "network.port", "client.address")
Definition schema.h:50
void(* format_output)(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf, size_t bufsize)
Format for TOML output.
Definition config.c:154
Options configuration.
Definition builder.h:401
Consolidated options structure.
#define ASCII_CHAT_VERSION_PATCH
Definition version.h:9
#define ASCII_CHAT_VERSION_MINOR
Definition version.h:8
#define ASCII_CHAT_VERSION_MAJOR
Definition version.h:7
#define ASCII_CHAT_GIT_VERSION
Definition version.h:11

References ASCII_CHAT_GIT_VERSION, ASCII_CHAT_VERSION_MAJOR, ASCII_CHAT_VERSION_MINOR, ASCII_CHAT_VERSION_PATCH, ASCIICHAT_OK, config_builder_t::buffer, BUFFER_SIZE_MEDIUM, config_builder_t::capacity, config_option_metadata_t::category, config_schema_build_from_configs(), config_schema_get_all(), config_schema_get_by_category(), defer, config_option_metadata_t::description, DIR_PERM_PRIVATE, ERROR_CONFIG, ERROR_MEMORY, expand_path(), config_option_metadata_t::field_offset, config_option_metadata_t::field_size, option_type_handler_t::format_output, log_plain, config_option_metadata_t::mode_bitmask, config_option_metadata_t::mode_default_getter, MODE_INVALID, OPTION_TYPE_CALLBACK, OPTION_TYPE_INT, OPTION_TYPE_STRING, OPTIONS_BUFF_SIZE, options_config_destroy(), options_preset_unified(), options_t_new(), PATH_DELIM, PATH_ROLE_CONFIG_FILE, path_validate_user_path(), platform_fopen(), platform_mkdir_recursive(), platform_prompt_yes_no(), platform_strdup(), platform_write_all(), SAFE_FCLOSE, SAFE_FREE, SAFE_MALLOC, SAFE_STRNCPY, SET_ERRNO, SET_ERRNO_SYS, config_builder_t::size, terminal_flush(), config_option_metadata_t::toml_key, and config_option_metadata_t::type.

Referenced by action_create_config(), and options_init().

◆ config_load_and_apply()

asciichat_error_t config_load_and_apply ( asciichat_mode_t  detected_mode,
const char *  config_path,
bool  strict,
options_t *  opts 
)

#include <config.c>

Main function to load configuration from file and apply to global options.

Load configuration from TOML file and apply to global options.

Parameters
is_clienttrue if loading client configuration, false for server configuration
config_pathOptional path to config file (NULL uses default location)
strictIf true, errors are fatal; if false, errors are non-fatal warnings
Returns
ASCIICHAT_OK on success, error code on failure (if strict) or non-fatal (if !strict)

This is the main entry point for configuration loading. It:

  1. Expands the config file path (default location or custom path)
  2. Checks if the file exists and is a regular file
  3. Parses the TOML file using tomlc17
  4. Applies configuration from each section (network, client, palette, crypto, logging)
  5. Frees resources and returns

Configuration file errors are non-fatal if strict is false:

  • Missing file: Returns ASCIICHAT_OK (config file is optional)
  • Not a regular file: Warns and returns ASCIICHAT_OK
  • Parse errors: Warns and returns ASCIICHAT_OK
  • Invalid values: Individual values are skipped with warnings

If strict is true, any error causes immediate return with error code.

Note
This function should be called before options_init() parses command-line arguments to ensure CLI arguments can override config file values.
Configuration warnings are printed to stderr because logging may not be initialized yet when this function is called.
Parameters
is_clienttrue if loading client configuration, false for server configuration
config_pathOptional path to config file (NULL uses default location)
strictIf true, errors are fatal; if false, errors are non-fatal warnings
Returns
ASCIICHAT_OK on success, error code on failure (if strict) or non-fatal (if !strict)

Loads configuration from the specified path (or default location if config_path is NULL) and applies values to global options.

Default config file location (when config_path is NULL):

  • Unix: $XDG_CONFIG_HOME/ascii-chat/config.toml if set, otherwise ~/.ascii-chat/config.toml
  • Windows: APPDATA%\ascii-chat\config.toml if set, otherwise ~.ascii-chat\config.toml

Only applies configuration values that haven't already been set (though in practice, CLI arguments will override config values anyway since this is called before CLI parsing).

Supported configuration sections:

  • [network]: port
  • [server]: bind_ipv4, bind_ipv6
  • [client]: address, width, height, webcam_index, flip_x, flip_y, color_mode, render_mode, fps, stretch, quiet, snapshot_mode, snapshot_delay, test_pattern, show_capabilities, force_utf8
  • [audio]: enabled, device
  • [palette]: type, chars
  • [crypto]: encrypt_enabled, key, password, keyfile, no_encrypt, server_key (client only), client_keys (server only)
  • [logging] or root: log_file
Note
This function should be called before options_init() parses command-line arguments, so that CLI arguments can override config file values.
If strict is false and the config file doesn't exist, is not a regular file, or fails to parse, the function returns ASCIICHAT_OK (non-fatal). Individual invalid values are skipped with warnings, but valid values are still applied.
If strict is true, any error (file not found, parse error, etc.) causes the function to return an error code immediately.
Warning
Configuration warnings are printed directly to stderr because logging may not be initialized yet when this function is called.

Definition at line 1041 of file config.c.

1042 {
1043 // detected_mode is used in config_apply_schema for bitmask validation
1044 char *config_path_expanded = NULL;
1045 defer(SAFE_FREE(config_path_expanded));
1046
1047 if (config_path) {
1048 // Use custom path provided
1049 config_path_expanded = expand_path(config_path);
1050 if (!config_path_expanded) {
1051 // If expansion fails, try using as-is (might already be absolute)
1052 config_path_expanded = platform_strdup(config_path);
1053 }
1054 } else {
1055 // Use default location with XDG support
1056 char *config_dir = get_config_dir();
1057 defer(SAFE_FREE(config_dir));
1058 if (config_dir) {
1059 size_t len = strlen(config_dir) + strlen("config.toml") + 1;
1060 config_path_expanded = SAFE_MALLOC(len, char *);
1061 if (config_path_expanded) {
1062 safe_snprintf(config_path_expanded, len, "%sconfig.toml", config_dir);
1063 }
1064 }
1065
1066 // Fallback to ~/.ascii-chat/config.toml
1067 if (!config_path_expanded) {
1068 config_path_expanded = expand_path("~/.ascii-chat/config.toml");
1069 }
1070 }
1071
1072 if (!config_path_expanded) {
1073 if (strict) {
1074 return SET_ERRNO(ERROR_CONFIG, "Failed to resolve config file path");
1075 }
1076 return ASCIICHAT_OK;
1077 }
1078
1079 char *validated_config_path = NULL;
1080 // An optional default config is allowed not to exist. Check this before the
1081 // whitelist validator, which quite correctly rejects paths whose parent
1082 // directory has not been created yet but should not turn that normal case
1083 // into a configuration error.
1084 if (!strict && access(config_path_expanded, F_OK) != 0 && errno == ENOENT) {
1085 return ASCIICHAT_OK;
1086 }
1087 asciichat_error_t validate_result =
1088 path_validate_user_path(config_path_expanded, PATH_ROLE_CONFIG_FILE, &validated_config_path);
1089 if (validate_result != ASCIICHAT_OK) {
1090 SAFE_FREE(validated_config_path);
1091 SAFE_FREE(config_path_expanded);
1092 return validate_result;
1093 }
1094 // Free the old path before reassigning (defer will free the new one)
1095 if (config_path_expanded != validated_config_path) {
1096 SAFE_FREE(config_path_expanded);
1097 }
1098 config_path_expanded = validated_config_path;
1099
1100 // Determine display path for error messages (before any early returns)
1101 const char *display_path = config_path ? config_path : config_path_expanded;
1102
1103 // Log that we're attempting to load config (before logging is initialized, use stderr)
1104 // Only print if terminal output is enabled (suppress with --quiet)
1105 if (config_path && log_get_terminal_output()) {
1106 log_debug("Loading configuration from: %s", display_path);
1107 }
1108
1109 // Check if config file exists
1110 struct stat st;
1111 if (stat(config_path_expanded, &st) != 0) {
1112 if (strict) {
1113 return SET_ERRNO(ERROR_CONFIG, "Config file does not exist: '%s'", display_path);
1114 }
1115 // File doesn't exist, that's OK - not required (non-strict mode)
1116 return ASCIICHAT_OK;
1117 }
1118
1119 // Verify it's a regular file
1120 if (!S_ISREG(st.st_mode)) {
1121 if (strict) {
1122 return SET_ERRNO(ERROR_CONFIG, "Config file exists but is not a regular file: '%s'", display_path);
1123 }
1124 CONFIG_WARN("Config file exists but is not a regular file: '%s' (skipping)", display_path);
1125 return ASCIICHAT_OK;
1126 }
1127
1128 // Parse TOML file
1129 toml_result_t result = toml_parse_file_ex(config_path_expanded);
1130 // Ensure TOML resources are freed at ALL function exit points (defer handles cleanup)
1131 defer(toml_free(result));
1132
1133 if (!result.ok) {
1134 // result.errmsg is an array, so check its first character
1135 const char *errmsg = (strlen(result.errmsg) > 0) ? result.errmsg : "Unknown parse error";
1136
1137 if (strict) {
1138 // For strict mode, return detailed error message directly
1139 // Note: SET_ERRNO stores the message in context, but asciichat_error_string() only returns generic codes
1140 // So we need to format the error message ourselves here
1141 char error_buffer[BUFFER_SIZE_MEDIUM];
1142 safe_snprintf(error_buffer, sizeof(error_buffer), "Failed to parse config file '%s': %s", display_path, errmsg);
1143 toml_free(result); // Explicit cleanup before return (defer transformation not applied)
1144 return SET_ERRNO(ERROR_CONFIG, "%s", error_buffer);
1145 }
1146 CONFIG_WARN("Failed to parse config file '%s': %s (skipping)", display_path, errmsg);
1147 toml_free(result); // Explicit cleanup before return (defer transformation not applied)
1148 return ASCIICHAT_OK; // Non-fatal error
1149 }
1150
1151 // Apply configuration using schema-driven parser with bitmask validation
1152 asciichat_error_t schema_result = config_apply_schema(result.toptab, detected_mode, opts, strict);
1153
1154 if (schema_result != ASCIICHAT_OK && strict) {
1155 toml_free(result); // Explicit cleanup before return (defer transformation not applied)
1156 return schema_result;
1157 }
1158 // In non-strict mode, continue even if some options failed validation
1159
1160 CONFIG_DEBUG("Loaded configuration from %s", display_path);
1161
1162 // Log successful config load (use stderr since logging may not be initialized yet)
1163 // Only print if terminal output is enabled (suppress with --quiet)
1165 log_debug("Loaded configuration from: %s", display_path);
1166 }
1167
1168 // Update RCU system with modified options (for test compatibility)
1169 // In real usage, options_state_set is called later after CLI parsing
1170 asciichat_error_t rcu_result = options_state_set(opts);
1171 if (rcu_result != ASCIICHAT_OK) {
1172 // Non-fatal - RCU might not be initialized yet in some test scenarios
1173 // But log as warning so tests can see if this is the issue
1174 CONFIG_WARN("Failed to update RCU options state: %d (values may not be persisted)", rcu_result);
1175 }
1176
1177 toml_free(result); // Explicit cleanup before return (defer transformation not applied)
1178 return ASCIICHAT_OK;
1179}
#define CONFIG_DEBUG(fmt,...)
Print configuration debug message.
Definition config.c:57
#define CONFIG_WARN(fmt,...)
Print configuration warning using the logging system.
Definition config.c:46
bool log_get_terminal_output(void)
Get current terminal output setting.
Definition log/log.c:717
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
int errno
char * get_config_dir(void)
Get configuration directory path with XDG_CONFIG_HOME support.
Definition path.c:527
asciichat_error_t options_state_set(const options_t *opts)
Set options from a parsed options struct.
Definition rcu.c:439

References ASCIICHAT_OK, BUFFER_SIZE_MEDIUM, CONFIG_DEBUG, CONFIG_WARN, defer, errno, ERROR_CONFIG, expand_path(), get_config_dir(), log_debug, log_get_terminal_output(), options_state_set(), PATH_ROLE_CONFIG_FILE, path_validate_user_path(), platform_strdup(), SAFE_FREE, SAFE_MALLOC, safe_snprintf(), and SET_ERRNO.

Referenced by config_load_system_and_user().

◆ config_load_system_and_user()

asciichat_error_t config_load_system_and_user ( asciichat_mode_t  detected_mode,
bool  strict,
options_t *  opts 
)

#include <config.h>

Load system config first, then user config (user config overrides system)

Parameters
detected_modeThe detected mode (client or server)
strictIf true, user config errors are fatal; system config is always non-strict
optsOptions structure to write configuration to
Returns
ASCIICHAT_OK on success, error code on failure

Loads configuration from two locations in order:

  1. System config: ${INSTALL_PREFIX}/etc/ascii-chat/config.toml (non-strict, optional)
  2. User config: default location (strictness as specified)

User config values override system config values. Both override defaults. This function should be called before options_init() parses command-line arguments.

Note
System config is always loaded non-strict (missing file is not an error)
User config strictness follows the strict parameter

Definition at line 1547 of file config.c.

1547 {
1548 // Use platform abstraction to find all config.toml files across standard locations
1549 config_file_list_t config_files = {0};
1550 asciichat_error_t search_result = platform_find_config_file("config.toml", &config_files);
1551 defer(config_file_list_destroy(&config_files));
1552
1553 if (search_result != ASCIICHAT_OK) {
1554 CONFIG_DEBUG("Failed to search for config files: %d", search_result);
1555 config_file_list_destroy(&config_files);
1556 return search_result;
1557 }
1558
1559 // Cascade load: Load all found configs in reverse order (lowest priority first)
1560 // This allows higher-priority configs to override lower-priority values.
1561 // Example: System configs load first, then user configs override them.
1562
1564 for (size_t i = config_files.count; i > 0; i--) {
1565 const config_file_result_t *file = &config_files.files[i - 1];
1566
1567 // Determine strictness based on whether this is a system or user config
1568 // System configs are non-strict (values can be missing, errors are non-fatal)
1569 // User config is strict or non-strict based on parameter
1570 bool is_user_config = !file->is_system_config;
1571 bool file_strict = is_user_config ? strict : false;
1572
1573 CONFIG_DEBUG("Loading config from %s (system=%s, strict=%s)", file->path, file->is_system_config ? "yes" : "no",
1574 file_strict ? "true" : "false");
1575
1576 asciichat_error_t load_result = config_load_and_apply(detected_mode, file->path, file_strict, opts);
1577
1578 if (load_result != ASCIICHAT_OK) {
1579 if (file_strict) {
1580 // Strict mode: errors are fatal
1581 CONFIG_DEBUG("Strict config loading failed for %s", file->path);
1582 result = load_result;
1583 } else {
1584 // Non-strict mode: errors are non-fatal, just log and continue
1585 CONFIG_DEBUG("Non-strict config loading warning for %s: %d (continuing)", file->path, load_result);
1586 CLEAR_ERRNO(); // Clear error context for next file
1587 }
1588 }
1589 }
1590
1591 config_file_list_destroy(&config_files);
1592 return result;
1593}
asciichat_error_t config_load_and_apply(asciichat_mode_t detected_mode, const char *config_path, bool strict, options_t *opts)
Main function to load configuration from file and apply to global options.
Definition config.c:1041
#define CLEAR_ERRNO()
Clear the current error state.
asciichat_error_t platform_find_config_file(const char *filename, config_file_list_t *list_out)
Find config file across multiple standard locations.
void config_file_list_destroy(config_file_list_t *list)
Free config file list resources.
List of config file search results.
Definition filesystem.h:595
size_t count
Number of results found.
Definition filesystem.h:597
config_file_result_t * files
Array of results (allocated, must be freed)
Definition filesystem.h:596
Result of a config file search.
Definition filesystem.h:582
char * path
Absolute path to config file (allocated, must be freed)
Definition filesystem.h:583
bool is_system_config
True if from system directory (not user config)
Definition filesystem.h:586

References ASCIICHAT_OK, CLEAR_ERRNO, CONFIG_DEBUG, config_file_list_destroy(), config_load_and_apply(), config_file_list_t::count, defer, config_file_list_t::files, config_file_result_t::is_system_config, config_file_result_t::path, and platform_find_config_file().

Referenced by options_init().