ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
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:

  • TOML file parsing and validation
  • Automatic configuration loading from standard location
  • Non-fatal error handling (missing or malformed config files are ignored)
  • Support for network, client, palette, crypto, and logging configuration
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 <include/ascii-chat/options/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 1325 of file options/config/config.c.

1325 {
1326 char *config_path_expanded = NULL;
1327
1328 defer(SAFE_FREE(config_path_expanded));
1329
1330 // Create fresh options with all OPT_*_DEFAULT values
1331 options_t defaults = options_t_new();
1332
1333 // Allocate buffer for building config content (256KB should be plenty)
1334 const size_t BUFFER_CAPACITY = 256 * 1024;
1335 config_builder_t builder = {0};
1336 builder.buffer = SAFE_MALLOC(BUFFER_CAPACITY, char *);
1337 if (!builder.buffer) {
1338 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate config buffer");
1339 }
1340 defer(SAFE_FREE(builder.buffer));
1341 builder.capacity = BUFFER_CAPACITY;
1342
1343 // Build version comment in buffer
1344 if (!config_builder_append(&builder, "# ascii-chat configuration file\n")) {
1345 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1346 }
1347 if (!config_builder_append(&builder, "# Generated by ascii-chat v%d.%d.%d-%s\n", ASCII_CHAT_VERSION_MAJOR,
1348 ASCII_CHAT_VERSION_MINOR, ASCII_CHAT_VERSION_PATCH, ASCII_CHAT_GIT_VERSION)) {
1349 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1350 }
1351 if (!config_builder_append(&builder, "#\n")) {
1352 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1353 }
1354 if (!config_builder_append(&builder, "# All options below are commented out because some configuration options\n")) {
1355 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1356 }
1357 if (!config_builder_append(&builder, "# conflict with each other (e.g., --file vs --url, --loop vs --url).\n")) {
1358 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1359 }
1360 if (!config_builder_append(&builder, "# Uncomment only the options you need and avoid conflicting combinations.\n")) {
1361 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1362 }
1363 if (!config_builder_append(&builder, "#\n")) {
1364 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1365 }
1366 if (!config_builder_append(&builder,
1367 "# If you upgrade ascii-chat and this version comment changes, you may need to\n")) {
1368 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1369 }
1370 if (!config_builder_append(&builder, "# delete and regenerate this file with: ascii-chat --config-create\n")) {
1371 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1372 }
1373 if (!config_builder_append(&builder, "#\n\n")) {
1374 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1375 }
1376
1377 // Get all options from schema
1378 size_t metadata_count = 0;
1379 const config_option_metadata_t *metadata = config_schema_get_all(&metadata_count);
1380 {
1381 options_config_t *unified_config = options_preset_unified(NULL, NULL);
1382 if (unified_config) {
1383 const options_config_t *schema_config = unified_config;
1384 (void)config_schema_build_from_configs(&schema_config, 1);
1385 options_config_destroy(unified_config);
1386 metadata = config_schema_get_all(&metadata_count);
1387 }
1388 }
1389
1390 // Build list of unique categories in order of first appearance
1391 const char *categories[CONFIG_CATEGORY_CAPACITY] = {0}; // Max expected categories
1392 size_t category_count = 0;
1393
1394 for (size_t i = 0; i < metadata_count && category_count < CONFIG_CATEGORY_CAPACITY; i++) {
1395 const char *category = metadata[i].category;
1396 if (!category) {
1397 continue;
1398 }
1399
1400 // Check if category already in list
1401 bool found = false;
1402 for (size_t j = 0; j < category_count; j++) {
1403 if (categories[j] && strcmp(categories[j], category) == 0) {
1404 found = true;
1405 break;
1406 }
1407 }
1408
1409 if (!found) {
1410 categories[category_count++] = category;
1411 }
1412 }
1413
1414 // Build each section dynamically from schema
1415 for (size_t cat_idx = 0; cat_idx < category_count; cat_idx++) {
1416 const char *category = categories[cat_idx];
1417 if (!category) {
1418 continue;
1419 }
1420
1421 // Get all options for this category
1422 size_t cat_option_count = 0;
1423 const config_option_metadata_t **cat_options = config_schema_get_by_category(category, &cat_option_count);
1424
1425 if (!cat_options || cat_option_count == 0) {
1426 continue;
1427 }
1428
1429 // Add section header
1430 if (!config_builder_append(&builder, "[%s]\n", category)) {
1431 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1432 }
1433
1434 // Track which options we've written (to avoid duplicates)
1435 bool written_flags[CONFIG_WRITTEN_FLAG_CAPACITY] = {0}; // Max options per category
1436
1437 // Add each option in this category
1438 for (size_t opt_idx = 0; opt_idx < cat_option_count && opt_idx < 64; opt_idx++) {
1439 const config_option_metadata_t *meta = cat_options[opt_idx];
1440 if (!meta || !meta->toml_key) {
1441 continue;
1442 }
1443
1444 // Skip if already written (duplicate)
1445 if (written_flags[opt_idx]) {
1446 continue;
1447 }
1448
1449 // Skip if this is a duplicate of another option (check by TOML key, not field_offset)
1450 // Note: Multiple options can map to the same field (e.g., server_log_file, client_log_file)
1451 bool is_duplicate = false;
1452 for (size_t j = 0; j < opt_idx; j++) {
1453 if (cat_options[j] && cat_options[j]->toml_key && meta->toml_key &&
1454 strcmp(cat_options[j]->toml_key, meta->toml_key) == 0) {
1455 is_duplicate = true;
1456 break;
1457 }
1458 }
1459 if (is_duplicate) {
1460 continue;
1461 }
1462
1463 // Get field pointer from default options (or mode-specific default if available)
1464 const char *field_ptr = ((const char *)&defaults) + meta->field_offset;
1465
1466 // Buffer to hold mode-specific default value if needed
1467 char mode_default_buffer[OPTIONS_BUFF_SIZE] = {0};
1468 int mode_default_int = 0;
1469
1470 // If this option has a mode_default_getter, use it to get the correct default
1471 if (meta->mode_default_getter) {
1472 asciichat_mode_t mode = extract_mode_from_bitmask(meta->mode_bitmask);
1473 if (mode != MODE_INVALID) {
1474 const void *default_value = meta->mode_default_getter(mode);
1475 if (default_value) {
1476 // Copy the default value to our buffer based on type
1477 if (meta->type == OPTION_TYPE_STRING || meta->type == OPTION_TYPE_CALLBACK) {
1478 const char *str_value = (const char *)default_value;
1479 SAFE_STRNCPY(mode_default_buffer, str_value, sizeof(mode_default_buffer));
1480 field_ptr = mode_default_buffer;
1481 } else if (meta->type == OPTION_TYPE_INT) {
1482 mode_default_int = *(const int *)default_value;
1483 field_ptr = (const char *)&mode_default_int;
1484 }
1485 }
1486 }
1487 }
1488
1489 // Add description comment if available
1490 if (meta->description && strlen(meta->description) > 0) {
1491 if (!config_builder_append(&builder, "# %s\n", meta->description)) {
1492 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1493 }
1494 }
1495
1496 // Format and add the option value using handler (commented out to avoid conflicts)
1497 if (g_type_handlers[meta->type].format_output) {
1498 char formatted_value[BUFFER_SIZE_MEDIUM] = {0};
1499 g_type_handlers[meta->type].format_output(field_ptr, meta->field_size, meta, formatted_value,
1500 sizeof(formatted_value));
1501 const char *output_key = meta->toml_key;
1502 size_t category_len = strlen(category);
1503 if (strncmp(meta->toml_key, category, category_len) == 0 && meta->toml_key[category_len] == '.') {
1504 output_key = meta->toml_key + category_len + 1; // Strip "<category>."
1505 }
1506
1507 if (config_key_should_be_commented(meta->toml_key)) {
1508 if (!config_builder_append(&builder, "# %s = %s\n", output_key, formatted_value)) {
1509 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1510 }
1511 } else {
1512 if (!config_builder_append(&builder, "%s = %s\n", output_key, formatted_value)) {
1513 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1514 }
1515 }
1516 }
1517
1518 // Add blank line after each option
1519 if (!config_builder_append(&builder, "\n")) {
1520 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1521 }
1522
1523 written_flags[opt_idx] = true;
1524 }
1525
1526 // Add blank line between sections (but not after the last section)
1527 if (cat_idx < category_count - 1) {
1528 if (!config_builder_append(&builder, "\n")) {
1529 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1530 }
1531 }
1532 }
1533
1534 // Now write the buffer to either stdout or a file
1535 if (config_path && strlen(config_path) > 0) {
1536 // User provided a filepath - write to that file with overwrite prompt
1537
1538 // Expand and validate the path
1539 config_path_expanded = expand_path(config_path);
1540 if (!config_path_expanded) {
1541 config_path_expanded = platform_strdup(config_path);
1542 }
1543
1544 if (!config_path_expanded) {
1545 return SET_ERRNO(ERROR_CONFIG, "Failed to resolve config file path");
1546 }
1547
1548 char *validated_config_path = NULL;
1549 asciichat_error_t validate_result =
1550 path_validate_user_path(config_path_expanded, PATH_ROLE_CONFIG_FILE, &validated_config_path);
1551 if (validate_result != ASCIICHAT_OK) {
1552 SAFE_FREE(validated_config_path);
1553 SAFE_FREE(config_path_expanded);
1554 return validate_result;
1555 }
1556 // Free the old path before reassigning (defer will free the new one)
1557 if (config_path_expanded != validated_config_path) {
1558 SAFE_FREE(config_path_expanded);
1559 }
1560 config_path_expanded = validated_config_path;
1561
1562 // Check if file already exists
1563 struct stat st;
1564 if (stat(config_path_expanded, &st) == 0) {
1565 // File exists - ask user if they want to overwrite
1566 log_plain("Config file already exists: %s", config_path_expanded);
1567
1569 safe_snprintf(question, sizeof(question), "File already exists: %s\n\nOverwrite", config_path_expanded);
1570 bool overwrite =
1571 ui_notice_confirm_kind(UI_PROMPT_CONFIG_OVERWRITE, UI_NOTICE_WARNING, question, 30); // Default to No
1572 if (!overwrite) {
1573 log_plain("Config file creation cancelled.");
1574 return SET_ERRNO(ERROR_CONFIG, "User cancelled overwrite");
1575 }
1576
1577 log_plain("Overwriting existing config file...");
1578 }
1579
1580 // Create directory if needed
1581 char *dir_path = platform_strdup(config_path_expanded);
1582 if (!dir_path) {
1583 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate memory for directory path");
1584 }
1585 defer(SAFE_FREE(dir_path));
1586
1587 // Find the last path separator
1588 char *last_sep = strrchr(dir_path, PATH_DELIM);
1589
1590 if (last_sep) {
1591 *last_sep = '\0';
1592 // Create directory recursively
1593 asciichat_error_t mkdir_result = platform_mkdir_recursive(dir_path, DIR_PERM_PRIVATE);
1594 if (mkdir_result != ASCIICHAT_OK) {
1595 return mkdir_result;
1596 }
1597 }
1598
1599 // Open file for writing
1600 FILE *output_file = platform_fopen("file_stream", config_path_expanded, "w");
1601 if (!output_file) {
1602 return SET_ERRNO_SYS(ERROR_CONFIG, "Failed to open config file for writing: %s", config_path_expanded);
1603 }
1604 defer(SAFE_FCLOSE(output_file));
1605
1606 // Write buffer to file
1607 size_t written = fwrite(builder.buffer, 1, builder.size, output_file);
1608 if (written != builder.size) {
1609 asciichat_error_t error = SET_ERRNO_SYS(ERROR_CONFIG, "Failed to write config to file: %s", config_path_expanded);
1610 SAFE_FCLOSE(output_file);
1611 return error;
1612 }
1613 // Publish buffered bytes before returning, including builds without defer instrumentation.
1614 int close_result = fclose(output_file);
1615 output_file = NULL;
1616 if (close_result != 0) {
1617 return SET_ERRNO_SYS(ERROR_CONFIG, "Failed to close config file: %s", config_path_expanded);
1618 }
1619 } else {
1620 // No filepath provided - write buffer to stdout with automatic retry on transient errors
1621 (void)platform_write_all(STDOUT_FILENO, builder.buffer, builder.size);
1622 // Flush C stdio buffer and terminal to ensure piped output is written immediately
1623 (void)fflush(stdout);
1624 (void)terminal_flush(STDOUT_FILENO);
1625 }
1626
1627 return ASCIICHAT_OK;
1628}
#define SET_ERRNO(code,...)
#define SET_ERRNO_SYS(code,...)
@ OPTION_TYPE_INT
Integer value (–count 42).
Definition builder.h:168
@ OPTION_TYPE_STRING
String value (–name foo).
Definition builder.h:169
@ OPTION_TYPE_CALLBACK
Custom parser function.
Definition builder.h:171
void options_config_destroy(options_config_t *config)
Free options config.
#define BUFFER_SIZE_MEDIUM
Medium buffer size (512 bytes).
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_MALLOC(size, cast)
Definition common.h:265
#define SAFE_FCLOSE(fp)
Definition common.h:395
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_MEMORY
Definition error_codes.h:51
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_CONFIG
Definition error_codes.h:52
#define log_plain(...)
Plain logging - writes to both log file and stderr without timestamps or log levels.
Definition log/log.h:665
asciichat_mode_t
Mode type for options parsing.
Definition options.h:919
struct options_state options_t
Immutable, RCU-published snapshot of the application's runtime configuration.
#define OPTIONS_BUFF_SIZE
Buffer size for option string values.
Definition options.h:310
options_t options_t_new(void)
Initialize options by parsing command-line arguments.
Definition options.c:547
@ MODE_INVALID
Invalid mode.
Definition options.h:927
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
size_t platform_write_all(int fd, const void *buf, size_t count)
Write all bytes to a file descriptor, handling partial writes.
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:712
char * expand_path(const char *path)
Expand path with tilde (~) support.
Definition path.c:259
@ PATH_ROLE_CONFIG_FILE
Definition path.h:319
bool ui_notice_confirm_kind(ui_prompt_kind_t kind, ui_notice_severity_t severity, const char *text, unsigned timeout_seconds)
Definition notice.c:498
@ UI_NOTICE_WARNING
Definition notice.h:11
#define CONFIG_CATEGORY_CAPACITY
#define CONFIG_WRITTEN_FLAG_CAPACITY
#define OPTIONS_CONFIG_QUESTION_BUFFER_SIZE
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:58
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:416
const config_option_metadata_t * config_schema_get_all(size_t *count)
Get all option metadata.
Definition schema.c:449
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:252
Helper structure for building config content in a buffer.
Option metadata for config file parsing.
Definition schema.h:48
option_type_t type
Value type (from builder).
Definition schema.h:51
size_t field_size
Size of field in options_t.
Definition schema.h:55
option_mode_bitmask_t mode_bitmask
Which modes this option applies to.
Definition schema.h:64
const void *(* mode_default_getter)(asciichat_mode_t mode)
Get mode-specific default value (NULL if not mode-aware).
Definition schema.h:82
size_t field_offset
offsetof(options_t, field) - where to store value
Definition schema.h:54
const char * description
Description for docs/help generation.
Definition schema.h:67
const char * category
Category name (e.g., "network", "client", "audio").
Definition schema.h:53
const char * toml_key
TOML key path (e.g., "network.port", "client.address").
Definition schema.h:49
Immutable parsing tables produced from an options builder.
Definition builder.h:414

References ASCIICHAT_OK, config_builder_t::buffer, BUFFER_SIZE_MEDIUM, config_builder_t::capacity, config_option_metadata_t::category, CONFIG_CATEGORY_CAPACITY, config_schema_build_from_configs(), config_schema_get_all(), config_schema_get_by_category(), CONFIG_WRITTEN_FLAG_CAPACITY, config_option_metadata_t::description, ERROR_CONFIG, ERROR_MEMORY, expand_path(), config_option_metadata_t::field_offset, config_option_metadata_t::field_size, 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_CONFIG_QUESTION_BUFFER_SIZE, options_preset_unified(), options_t_new(), PATH_ROLE_CONFIG_FILE, path_validate_user_path(), platform_write_all(), SAFE_FCLOSE, SAFE_FREE, SAFE_MALLOC, safe_snprintf(), SAFE_STRNCPY, SET_ERRNO, SET_ERRNO_SYS, config_builder_t::size, config_option_metadata_t::toml_key, config_option_metadata_t::type, ui_notice_confirm_kind(), and UI_NOTICE_WARNING.

Referenced by action_create_config().

◆ 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 <lib/options/config/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:

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 1103 of file options/config/config.c.

1104 {
1105 // detected_mode is used in config_apply_schema for bitmask validation
1106 char *config_path_expanded = NULL;
1107 defer(SAFE_FREE(config_path_expanded));
1108
1109 if (config_path) {
1110 // Use custom path provided
1111 config_path_expanded = expand_path(config_path);
1112 if (!config_path_expanded) {
1113 // If expansion fails, try using as-is (might already be absolute)
1114 config_path_expanded = platform_strdup(config_path);
1115 }
1116 } else {
1117 // Use default location with XDG support
1118 char *config_dir = get_config_dir();
1119 defer(SAFE_FREE(config_dir));
1120 if (config_dir) {
1121 size_t len = strlen(config_dir) + strlen("config.toml") + 1;
1122 config_path_expanded = SAFE_MALLOC(len, char *);
1123 if (config_path_expanded) {
1124 safe_snprintf(config_path_expanded, len, "%sconfig.toml", config_dir);
1125 }
1126 }
1127
1128 // Fallback to ~/.ascii-chat/config.toml
1129 if (!config_path_expanded) {
1130 config_path_expanded = expand_path("~/.ascii-chat/config.toml");
1131 }
1132 }
1133
1134 if (!config_path_expanded) {
1135 if (strict) {
1136 return SET_ERRNO(ERROR_CONFIG, "Failed to resolve config file path");
1137 }
1138 return ASCIICHAT_OK;
1139 }
1140
1141 char *validated_config_path = NULL;
1142 // An optional default config is allowed not to exist. Check this before the
1143 // whitelist validator, which quite correctly rejects paths whose parent
1144 // directory has not been created yet but should not turn that normal case
1145 // into a configuration error.
1146 if (!strict && platform_access(config_path_expanded, PLATFORM_ACCESS_EXISTS) != 0 && errno == ENOENT) {
1147 SAFE_FREE(config_path_expanded);
1148 return ASCIICHAT_OK;
1149 }
1150 asciichat_error_t validate_result =
1151 path_validate_user_path(config_path_expanded, PATH_ROLE_CONFIG_FILE, &validated_config_path);
1152 if (validate_result != ASCIICHAT_OK) {
1153 SAFE_FREE(validated_config_path);
1154 SAFE_FREE(config_path_expanded);
1155 return validate_result;
1156 }
1157 // Free the old path before reassigning (defer will free the new one)
1158 if (config_path_expanded != validated_config_path) {
1159 SAFE_FREE(config_path_expanded);
1160 }
1161 config_path_expanded = validated_config_path;
1162
1163 // Determine display path for error messages (before any early returns)
1164 const char *display_path = config_path ? config_path : config_path_expanded;
1165
1166 // Log that we're attempting to load config (before logging is initialized, use stderr)
1167 // Only print if terminal output is enabled (suppress with --quiet)
1168 if (config_path && log_get_terminal_output()) {
1169 log_debug("Loading configuration from: %s", display_path);
1170 }
1171
1172 // Check if config file exists
1173 struct stat st;
1174 if (stat(config_path_expanded, &st) != 0) {
1175 if (strict) {
1176 return SET_ERRNO(ERROR_CONFIG, "Config file does not exist: '%s'", display_path);
1177 }
1178 // File doesn't exist, that's OK - not required (non-strict mode)
1179 return ASCIICHAT_OK;
1180 }
1181
1182 // Verify it's a regular file
1183 if (!S_ISREG(st.st_mode)) {
1184 if (strict) {
1185 return SET_ERRNO(ERROR_CONFIG, "Config file exists but is not a regular file: '%s'", display_path);
1186 }
1187 CONFIG_WARN("Config file exists but is not a regular file: '%s' (skipping)", display_path);
1188 return ASCIICHAT_OK;
1189 }
1190
1191 // Parse TOML file
1192 toml_result_t result = toml_parse_file_ex(config_path_expanded);
1193 // Ensure TOML resources are freed at ALL function exit points (defer handles cleanup)
1194 defer(toml_free(result));
1195
1196 if (!result.ok) {
1197 // result.errmsg is an array, so check its first character
1198 const char *errmsg = (strlen(result.errmsg) > 0) ? result.errmsg : "Unknown parse error";
1199
1200 if (strict) {
1201 // For strict mode, return detailed error message directly
1202 // Note: SET_ERRNO stores the message in context, but asciichat_error_string() only returns generic codes
1203 // So we need to format the error message ourselves here
1204 char error_buffer[BUFFER_SIZE_MEDIUM];
1205 safe_snprintf(error_buffer, sizeof(error_buffer), "Failed to parse config file '%s': %s", display_path, errmsg);
1206 return SET_ERRNO(ERROR_CONFIG, "%s", error_buffer);
1207 }
1208 CONFIG_WARN("Failed to parse config file '%s': %s (skipping)", display_path, errmsg);
1209 return ASCIICHAT_OK; // Non-fatal error
1210 }
1211
1212 // Apply configuration using schema-driven parser with bitmask validation
1214 asciichat_error_t schema_result = config_apply_schema(result.toptab, detected_mode, opts, strict);
1215
1216 if (schema_result != ASCIICHAT_OK && strict) {
1217 return schema_result;
1218 }
1219 if (strict && schema_result == ASCIICHAT_OK)
1220 ASSERT_NO_ERRNO_SINCE(schema_scope);
1222 // In non-strict mode, continue even if some options failed validation
1223
1224 CONFIG_DEBUG("Loaded configuration from %s", display_path);
1225
1226 // Log successful config load (use stderr since logging may not be initialized yet)
1227 // Only print if terminal output is enabled (suppress with --quiet)
1229 log_debug("Loaded configuration from: %s", display_path);
1230 }
1231
1232 // Update RCU system with modified options (for test compatibility)
1233 // In real usage, options_state_set is called later after CLI parsing
1235 asciichat_error_t rcu_result = options_state_set(opts);
1236 if (rcu_result != ASCIICHAT_OK) {
1237 // Non-fatal - RCU might not be initialized yet in some test scenarios
1238 // But log as warning so tests can see if this is the issue
1239 CONFIG_WARN("Failed to update RCU options state: %d (values may not be persisted)", rcu_result);
1240 } else {
1241 ASSERT_NO_ERRNO_SINCE(rcu_scope);
1242 }
1244
1245 return ASCIICHAT_OK;
1246}
void errno_scope_end(asciichat_errno_scope_t scope, asciichat_errno_outcome_t outcome)
asciichat_errno_scope_t errno_scope_begin(void)
#define ASSERT_NO_ERRNO_SINCE(scope)
@ ASCIICHAT_ERRNO_DISMISSED
bool log_get_terminal_output(void)
Get current terminal output setting.
Definition log.c:763
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
int errno
char * get_config_dir(void)
Get configuration directory path with XDG_CONFIG_HOME support.
Definition path.c:281
#define CONFIG_DEBUG(fmt,...)
Print configuration debug message.
#define CONFIG_WARN(fmt,...)
Print configuration warning using the logging system.
asciichat_error_t options_state_set(const options_t *opts)
Set options from a parsed options struct.
Definition rcu.c:450
When code needs to work with asciichat errno snapshot, it keeps total, copied here instead of scatter...

References ASCIICHAT_ERRNO_DISMISSED, ASCIICHAT_OK, ASSERT_NO_ERRNO_SINCE, BUFFER_SIZE_MEDIUM, CONFIG_DEBUG, CONFIG_WARN, errno, errno_scope_begin(), errno_scope_end(), ERROR_CONFIG, expand_path(), get_config_dir(), log_debug, log_get_terminal_output(), options_state_set(), PATH_ROLE_CONFIG_FILE, path_validate_user_path(), 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 <include/ascii-chat/options/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 1630 of file options/config/config.c.

1630 {
1631 // Use platform abstraction to find all config.toml files across standard locations
1632 config_file_list_t config_files = {0};
1633 asciichat_error_t search_result = platform_find_config_file("config.toml", &config_files);
1634 defer(config_file_list_destroy(&config_files));
1635
1636 if (search_result != ASCIICHAT_OK) {
1637 CONFIG_DEBUG("Failed to search for config files: %d", search_result);
1638 return search_result;
1639 }
1640
1641 // Cascade load: Load all found configs in reverse order (lowest priority first)
1642 // This allows higher-priority configs to override lower-priority values.
1643 // Example: System configs load first, then user configs override them.
1644
1646 for (size_t i = config_files.count; i > 0; i--) {
1647 const config_file_result_t *file = &config_files.files[i - 1];
1648
1649 // Determine strictness based on whether this is a system or user config
1650 // System configs are non-strict (values can be missing, errors are non-fatal)
1651 // User config is strict or non-strict based on parameter
1652 bool is_user_config = !file->is_system_config;
1653 bool file_strict = is_user_config ? strict : false;
1654
1655 CONFIG_DEBUG("Loading config from %s (system=%s, strict=%s)", file->path, file->is_system_config ? "yes" : "no",
1656 file_strict ? OPT_VALUE_TRUE : OPT_VALUE_FALSE);
1657
1659 asciichat_error_t load_result = config_load_and_apply(detected_mode, file->path, file_strict, opts);
1660
1661 if (load_result != ASCIICHAT_OK) {
1662 if (file_strict) {
1663 // Strict mode: errors are fatal
1664 CONFIG_DEBUG("Strict config loading failed for %s", file->path);
1665 result = load_result;
1666 } else {
1667 // Non-strict mode: errors are non-fatal, just log and continue
1668 CONFIG_DEBUG("Non-strict config loading warning for %s: %d (continuing)", file->path, load_result);
1670 ASSERT_NO_ERRNO_SINCE(load_scope);
1671 }
1672 } else {
1673 ASSERT_NO_ERRNO_SINCE(load_scope);
1675 }
1676 }
1677
1678 return result;
1679}
@ ASCIICHAT_ERRNO_HANDLED
#define OPT_VALUE_FALSE
Definition enums.h:79
#define OPT_VALUE_TRUE
Definition enums.h:78
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.

References ASCIICHAT_ERRNO_DISMISSED, ASCIICHAT_ERRNO_HANDLED, ASCIICHAT_OK, ASSERT_NO_ERRNO_SINCE, CONFIG_DEBUG, config_load_and_apply(), errno_scope_begin(), errno_scope_end(), OPT_VALUE_FALSE, and OPT_VALUE_TRUE.