ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
config.c
Go to the documentation of this file.
1
15#include <ascii-chat/common.h>
22#include <ascii-chat/log/log.h>
25#include <ascii-chat/version.h>
27
28#include <ascii-chat-deps/tomlc17/src/tomlc17.h>
29#include <string.h>
30#include <stdlib.h>
31#include <stdio.h>
32#include <errno.h>
33#include <sys/stat.h>
34
46#define CONFIG_WARN(fmt, ...) \
47 do { \
48 log_warn("Config file: " fmt, ##__VA_ARGS__); \
49 } while (0)
50
57#define CONFIG_DEBUG(fmt, ...) \
58 do { \
59 /* Debug messages are only shown in debug builds after logging is initialized */ \
60 /* Use log_debug which safely checks initialization itself */ \
61 log_debug(fmt, ##__VA_ARGS__); \
62 } while (0)
63
66// Configuration state tracking removed - not needed with schema-driven approach
67// CLI arguments always override config values, so no need to track what was set
68
69/* Validation functions are now provided by options/validation.h */
70
87static const char *get_toml_string_validated(toml_datum_t datum) {
88 if (datum.type != TOML_STRING) {
89 SET_ERRNO(ERROR_INVALID_PARAM, "not a toml string");
90 return NULL;
91 }
92
93 const char *str = datum.u.s;
94 if (!str) {
95 SET_ERRNO(ERROR_INVALID_PARAM, "no toml string");
96 return NULL;
97 }
98
99 // Validate UTF-8 encoding using utility function
100 if (!utf8_is_valid(str)) {
101 log_warn("Config value contains invalid UTF-8 sequence");
102 return NULL;
103 }
104
105 return str;
106}
107
110// ============================================================================
111// Type Handler Registry - Consolidates 4 duplicated switch statements
112// ============================================================================
113// TECHNICAL DEBT FIX: The code previously had 4 separate switch(meta->type)
114// blocks for extract, parse, write, and format operations. This consolidated
115// registry eliminates ~300 lines of code duplication while maintaining all
116// type-specific logic and special case handling.
117
121typedef union {
122 char str_value[OPTIONS_BUFF_SIZE]; // STRING type
123 int int_value; // INT type
124 bool bool_value; // BOOL type
125 float float_value; // DOUBLE type (float variant)
126 double double_value; // DOUBLE type (double variant)
128
145typedef struct {
146 void (*extract)(toml_datum_t datum, char *value_str, int *int_val, bool *bool_val, double *double_val,
147 bool *has_value);
148 asciichat_error_t (*parse_validate)(const char *value_str, const config_option_metadata_t *meta,
149 option_parsed_value_t *parsed, char *error_msg,
150 size_t error_size);
151 asciichat_error_t (*write_to_struct)(const option_parsed_value_t *parsed, const config_option_metadata_t *meta,
152 options_t *opts, char *error_msg,
153 size_t error_size);
154 void (*format_output)(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
155 size_t bufsize);
157
158// Forward declarations
159static void extract_string(toml_datum_t datum, char *value_str, int *int_val, bool *bool_val, double *double_val,
160 bool *has_value);
161static void extract_int(toml_datum_t datum, char *value_str, int *int_val, bool *bool_val, double *double_val,
162 bool *has_value);
163static void extract_bool(toml_datum_t datum, char *value_str, int *int_val, bool *bool_val, double *double_val,
164 bool *has_value);
165static void extract_double(toml_datum_t datum, char *value_str, int *int_val, bool *bool_val, double *double_val,
166 bool *has_value);
167static asciichat_error_t parse_validate_string(const char *value_str, const config_option_metadata_t *meta,
168 option_parsed_value_t *parsed, char *error_msg, size_t error_size);
169static asciichat_error_t parse_validate_int(const char *value_str, const config_option_metadata_t *meta,
170 option_parsed_value_t *parsed, char *error_msg, size_t error_size);
171static asciichat_error_t parse_validate_bool(const char *value_str, const config_option_metadata_t *meta,
172 option_parsed_value_t *parsed, char *error_msg, size_t error_size);
173static asciichat_error_t parse_validate_double(const char *value_str, const config_option_metadata_t *meta,
174 option_parsed_value_t *parsed, char *error_msg, size_t error_size);
175static asciichat_error_t write_string(const option_parsed_value_t *parsed, const config_option_metadata_t *meta,
176 options_t *opts, char *error_msg, size_t error_size);
177static asciichat_error_t write_int(const option_parsed_value_t *parsed, const config_option_metadata_t *meta,
178 options_t *opts, char *error_msg, size_t error_size);
179static asciichat_error_t write_bool(const option_parsed_value_t *parsed, const config_option_metadata_t *meta,
180 options_t *opts, char *error_msg, size_t error_size);
181static asciichat_error_t write_double(const option_parsed_value_t *parsed, const config_option_metadata_t *meta,
182 options_t *opts, char *error_msg, size_t error_size);
183static void format_string(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
184 size_t bufsize);
185static void format_int(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
186 size_t bufsize);
187static void format_bool(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
188 size_t bufsize);
189static void format_double(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
190 size_t bufsize);
191static void format_callback(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
192 size_t bufsize);
193
194// Handler registry - indexed by option_type_t
195static const option_type_handler_t g_type_handlers[] = {
196 [OPTION_TYPE_STRING] = {extract_string, parse_validate_string, write_string, format_string},
197 [OPTION_TYPE_INT] = {extract_int, parse_validate_int, write_int, format_int},
198 [OPTION_TYPE_BOOL] = {extract_bool, parse_validate_bool, write_bool, format_bool},
199 [OPTION_TYPE_DOUBLE] = {extract_double, parse_validate_double, write_double, format_double},
200 [OPTION_TYPE_CALLBACK] = {NULL, NULL, NULL, format_callback},
201 [OPTION_TYPE_ACTION] = {NULL, NULL, NULL, NULL},
202};
203
204// ============================================================================
205// Type Handler Implementations
206// ============================================================================
207
211static void extract_string(toml_datum_t datum, char *value_str, int *int_val, bool *bool_val, double *double_val,
212 bool *has_value) {
213 (void)int_val;
214 (void)bool_val;
215 (void)double_val;
216
217 if (datum.type == TOML_STRING) {
218 const char *str = get_toml_string_validated(datum);
219 if (str && strlen(str) > 0) {
220 SAFE_STRNCPY(value_str, str, BUFFER_SIZE_MEDIUM);
221 *has_value = true;
222 }
223 } else if (datum.type == TOML_INT64) {
224 // Convert integer to string (e.g., port = 7777)
225 SAFE_SNPRINTF(value_str, BUFFER_SIZE_MEDIUM, "%lld", (long long)datum.u.int64);
226 *has_value = true;
227 }
228}
229
233static void extract_int(toml_datum_t datum, char *value_str, int *int_val, bool *bool_val, double *double_val,
234 bool *has_value) {
235 (void)bool_val;
236 (void)double_val;
237
238 if (datum.type == TOML_INT64) {
239 *int_val = (int)datum.u.int64;
240 SAFE_SNPRINTF(value_str, BUFFER_SIZE_MEDIUM, "%d", *int_val);
241 *has_value = true;
242 } else if (datum.type == TOML_STRING) {
243 const char *str = get_toml_string_validated(datum);
244 if (str) {
245 SAFE_STRNCPY(value_str, str, BUFFER_SIZE_MEDIUM);
246 *has_value = true;
247 }
248 }
249}
250
254static void extract_bool(toml_datum_t datum, char *value_str, int *int_val, bool *bool_val, double *double_val,
255 bool *has_value) {
256 (void)int_val;
257 (void)double_val;
258
259 if (datum.type == TOML_BOOLEAN) {
260 *bool_val = datum.u.boolean;
261 // Also set value_str for parse_validate phase
262 SAFE_STRNCPY(value_str, *bool_val ? "true" : "false", BUFFER_SIZE_MEDIUM);
263 *has_value = true;
264 }
265}
266
270static void extract_double(toml_datum_t datum, char *value_str, int *int_val, bool *bool_val, double *double_val,
271 bool *has_value) {
272 (void)int_val;
273 (void)bool_val;
274
275 if (datum.type == TOML_FP64) {
276 *double_val = datum.u.fp64;
277 SAFE_SNPRINTF(value_str, BUFFER_SIZE_MEDIUM, "%.10g", *double_val);
278 *has_value = true;
279 } else if (datum.type == TOML_STRING) {
280 const char *str = get_toml_string_validated(datum);
281 if (str) {
282 SAFE_STRNCPY(value_str, str, BUFFER_SIZE_MEDIUM);
283 *has_value = true;
284 }
285 }
286}
287
291static asciichat_error_t parse_validate_string(const char *value_str, const config_option_metadata_t *meta,
292 option_parsed_value_t *parsed, char *error_msg, size_t error_size) {
293 (void)meta;
294 (void)error_msg;
295 (void)error_size;
296
297 SAFE_STRNCPY(parsed->str_value, value_str, sizeof(parsed->str_value));
298 return ASCIICHAT_OK;
299}
300
304static asciichat_error_t parse_validate_int(const char *value_str, const config_option_metadata_t *meta,
305 option_parsed_value_t *parsed, char *error_msg, size_t error_size) {
306 // Detect enum fields by checking field_offset
307 int enum_val = -1;
308 bool is_enum = false;
309
310 if (meta->field_offset == offsetof(options_t, color_mode)) {
311 enum_val = validate_opt_color_mode(value_str, error_msg, error_size);
312 is_enum = true;
313 } else if (meta->field_offset == offsetof(options_t, color_filter)) {
314 color_filter_t filter = color_filter_from_cli_name(value_str);
315 if (filter != COLOR_FILTER_NONE || strcmp(value_str, "none") == 0) {
316 enum_val = (int)filter;
317 } else {
318 SAFE_SNPRINTF(error_msg, error_size, "Invalid color filter '%s'", value_str);
319 enum_val = -1;
320 }
321 is_enum = true;
322 } else if (meta->field_offset == offsetof(options_t, render_mode)) {
323 enum_val = validate_opt_render_mode(value_str, error_msg, error_size);
324 is_enum = true;
325 } else if (meta->field_offset == offsetof(options_t, palette_type)) {
326 enum_val = validate_opt_palette(value_str, error_msg, error_size);
327 is_enum = true;
328 }
329
330 if (is_enum) {
331 // Enum parsing
332 if (enum_val < 0) {
333 // Backward compatibility: accept numeric enum values too.
334 char *endptr = NULL;
335 long raw_enum = strtol(value_str, &endptr, 10);
336 bool parsed_numeric = (value_str && *value_str != '\0' && endptr && *endptr == '\0');
337 bool numeric_valid = false;
338
339 if (parsed_numeric && raw_enum >= INT_MIN && raw_enum <= INT_MAX) {
340 int enum_int = (int)raw_enum;
341 if (meta->field_offset == offsetof(options_t, color_mode)) {
342 numeric_valid =
343 (enum_int == COLOR_MODE_AUTO || enum_int == COLOR_MODE_NONE || enum_int == COLOR_MODE_16_COLOR ||
344 enum_int == COLOR_MODE_256_COLOR || enum_int == COLOR_MODE_TRUECOLOR);
345 } else if (meta->field_offset == offsetof(options_t, color_filter)) {
346 numeric_valid = (enum_int >= COLOR_FILTER_NONE && enum_int < COLOR_FILTER_COUNT);
347 } else if (meta->field_offset == offsetof(options_t, render_mode)) {
348 numeric_valid = (enum_int == RENDER_MODE_FOREGROUND || enum_int == RENDER_MODE_BACKGROUND ||
349 enum_int == RENDER_MODE_HALF_BLOCK);
350 } else if (meta->field_offset == offsetof(options_t, palette_type)) {
351 numeric_valid = (enum_int >= PALETTE_STANDARD && enum_int < PALETTE_COUNT);
352 }
353 if (numeric_valid) {
354 enum_val = enum_int;
355 }
356 }
357
358 if (enum_val < 0) {
359 if (strlen(error_msg) == 0) {
360 SAFE_SNPRINTF(error_msg, error_size, "Invalid enum value: %s", value_str);
361 }
362 return ERROR_CONFIG;
363 }
364 }
365 parsed->int_value = enum_val;
366 } else {
367 // Regular integer parsing
368 char *endptr = NULL;
369 long parsed_val = strtol(value_str, &endptr, 10);
370 if (*endptr != '\0') {
371 SAFE_SNPRINTF(error_msg, error_size, "Invalid integer: %s", value_str);
372 return ERROR_CONFIG;
373 }
374 if (parsed_val < INT_MIN || parsed_val > INT_MAX) {
375 SAFE_SNPRINTF(error_msg, error_size, "Integer out of range: %s", value_str);
376 return ERROR_CONFIG;
377 }
378
379 int int_val = (int)parsed_val;
380
381 // Check numeric range constraints if defined
382 // Constraints are present if max != 0 (max will always be non-zero for bounded integer ranges)
383 if (meta && meta->constraints.int_range.max != 0) {
384 // Allow 0 as special "use default" value for certain fields
385 bool is_auto_detect_field =
386 (meta->field_offset == offsetof(options_t, width) || meta->field_offset == offsetof(options_t, height) ||
387 meta->field_offset == offsetof(options_t, fps) ||
388 meta->field_offset == offsetof(options_t, compression_level));
389
390 if (int_val == 0 && is_auto_detect_field) {
391 // 0 is allowed for these fields (means auto-detect or use default)
392 } else if (int_val < meta->constraints.int_range.min || int_val > meta->constraints.int_range.max) {
393 SAFE_SNPRINTF(error_msg, error_size, "Integer %d out of range [%d-%d]: %s", int_val,
394 meta->constraints.int_range.min, meta->constraints.int_range.max, value_str);
395 return ERROR_CONFIG;
396 }
397 }
398
399 parsed->int_value = int_val;
400 }
401 return ASCIICHAT_OK;
402}
403
407static asciichat_error_t parse_validate_bool(const char *value_str, const config_option_metadata_t *meta,
408 option_parsed_value_t *parsed, char *error_msg, size_t error_size) {
409 (void)meta;
410 (void)error_msg;
411 (void)error_size;
412
413 // Parse boolean from string representation ("true" or "false")
414 parsed->bool_value =
415 value_str && (strcmp(value_str, "true") == 0 || strcmp(value_str, "1") == 0 || strcmp(value_str, "yes") == 0);
416 return ASCIICHAT_OK;
417}
418
422static asciichat_error_t parse_validate_double(const char *value_str, const config_option_metadata_t *meta,
423 option_parsed_value_t *parsed, char *error_msg, size_t error_size) {
424 char *endptr = NULL;
425 double parsed_val = strtod(value_str, &endptr);
426 if (*endptr != '\0') {
427 SAFE_SNPRINTF(error_msg, error_size, "Invalid float: %s", value_str);
428 return ERROR_CONFIG;
429 }
430
431 // Check numeric range constraints if defined (for double types)
432 if (meta && meta->constraints.int_range.max != 0) {
433 // Use int_range for doubles too (the values are stored as doubles in the constraint)
434 double min = (double)meta->constraints.int_range.min;
435 double max = (double)meta->constraints.int_range.max;
436 if (parsed_val < min || parsed_val > max) {
437 SAFE_SNPRINTF(error_msg, error_size, "Float %.2f out of range [%.2f-%.2f]: %s", parsed_val, min, max, value_str);
438 return ERROR_CONFIG;
439 }
440 }
441
442 parsed->float_value = (float)parsed_val;
443 return ASCIICHAT_OK;
444}
445
449static asciichat_error_t write_string(const option_parsed_value_t *parsed, const config_option_metadata_t *meta,
450 options_t *opts, char *error_msg, size_t error_size) {
451 (void)error_msg;
452 (void)error_size;
453
454 char *field_ptr = ((char *)opts) + meta->field_offset;
455 const char *final_value = parsed->str_value;
456
457 // Special handling for path-based options (keys, log files)
458 bool is_path_option = (strstr(meta->toml_key, "key") != NULL || strstr(meta->toml_key, "log_file") != NULL ||
459 strstr(meta->toml_key, "keyfile") != NULL);
460 if (is_path_option) {
461 // Check if it's a path that needs normalization
462 if (path_looks_like_path(final_value)) {
463 char *normalized = NULL;
464 path_role_t role = PATH_ROLE_CONFIG_FILE; // Default
465 if (strstr(meta->toml_key, "key") != NULL) {
466 role = (strstr(meta->toml_key, "server_key") != NULL || strstr(meta->toml_key, "client_keys") != NULL)
469 } else if (strstr(meta->toml_key, "log_file") != NULL) {
470 role = PATH_ROLE_LOG_FILE;
471 }
472
473 asciichat_error_t path_result = path_validate_user_path(final_value, role, &normalized);
474 if (path_result != ASCIICHAT_OK) {
475 SAFE_FREE(normalized);
476 return path_result;
477 }
478 SAFE_STRNCPY(field_ptr, normalized, meta->field_size);
479 SAFE_FREE(normalized);
480 } else {
481 // Not a path, just an identifier (e.g., "gpg:keyid", "github:user")
482 SAFE_STRNCPY(field_ptr, final_value, meta->field_size);
483 }
484
485 // Auto-enable encryption for crypto.key, crypto.password, crypto.keyfile
486 if (strstr(meta->toml_key, "crypto.key") != NULL || strstr(meta->toml_key, "crypto.password") != NULL ||
487 strstr(meta->toml_key, "crypto.keyfile") != NULL) {
488 opts->encrypt_enabled = 1;
489 }
490 } else {
491 SAFE_STRNCPY(field_ptr, final_value, meta->field_size);
492 }
493
494 return ASCIICHAT_OK;
495}
496
500static asciichat_error_t write_int(const option_parsed_value_t *parsed, const config_option_metadata_t *meta,
501 options_t *opts, char *error_msg, size_t error_size) {
502 (void)error_msg;
503 (void)error_size;
504
505 char *field_ptr = ((char *)opts) + meta->field_offset;
506
507 // Handle unsigned short int fields (webcam_index)
508 if (meta->field_size == sizeof(unsigned short int)) {
509 *(unsigned short int *)field_ptr = (unsigned short int)parsed->int_value;
510 } else {
511 *(int *)field_ptr = parsed->int_value;
512 }
513
514 return ASCIICHAT_OK;
515}
516
520static asciichat_error_t write_bool(const option_parsed_value_t *parsed, const config_option_metadata_t *meta,
521 options_t *opts, char *error_msg, size_t error_size) {
522 (void)error_msg;
523 (void)error_size;
524
525 char *field_ptr = ((char *)opts) + meta->field_offset;
526 bool bool_value = parsed->bool_value;
527 bool is_inverted_no_splash = false;
528 if (meta && meta->field_offset == offsetof(options_t, splash_screen) && meta->toml_key) {
529 const char *key = meta->toml_key;
530 size_t key_len = strlen(key);
531 static const char suffix[] = ".no_splash";
532 size_t suffix_len = sizeof(suffix) - 1;
533 is_inverted_no_splash = (key_len >= suffix_len && strcmp(key + (key_len - suffix_len), suffix) == 0);
534 }
535 if (is_inverted_no_splash) {
536 bool_value = !bool_value;
537 }
538
539 // Handle unsigned short int bool fields (common in options_t)
540 if (meta->field_size == sizeof(unsigned short int)) {
541 *(unsigned short int *)field_ptr = bool_value ? 1 : 0;
542 } else {
543 *(bool *)field_ptr = bool_value;
544 }
545
546 return ASCIICHAT_OK;
547}
548
552static asciichat_error_t write_double(const option_parsed_value_t *parsed, const config_option_metadata_t *meta,
553 options_t *opts, char *error_msg, size_t error_size) {
554 (void)error_msg;
555 (void)error_size;
556
557 char *field_ptr = ((char *)opts) + meta->field_offset;
558
559 // Check field_size to distinguish float from double
560 // Use memcpy for alignment-safe writes
561 if (meta->field_size == sizeof(float)) {
562 float float_val = (float)parsed->float_value;
563 memcpy(field_ptr, &float_val, sizeof(float));
564 } else {
565 double double_val = (double)parsed->float_value;
566 memcpy(field_ptr, &double_val, sizeof(double));
567 }
568
569 return ASCIICHAT_OK;
570}
571
575static void format_string(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
576 size_t bufsize) {
577 (void)field_size;
578 (void)meta;
579
580 const char *str_value = (const char *)field_ptr;
581 if (str_value && strlen(str_value) > 0) {
582 char escaped_str[BUFFER_SIZE_MEDIUM * 2]; // Max 2x size for escaping '%'
583 size_t j = 0;
584 for (size_t i = 0; i < strlen(str_value) && j < sizeof(escaped_str) - 2; ++i) {
585 if (str_value[i] == '%') {
586 escaped_str[j++] = '%';
587 escaped_str[j++] = '%';
588 } else {
589 escaped_str[j++] = str_value[i];
590 }
591 }
592 escaped_str[j] = '\0';
593
594 SAFE_SNPRINTF(buf, bufsize, "\"%s\"", escaped_str);
595 } else {
596 SAFE_SNPRINTF(buf, bufsize, "\"\"");
597 }
598}
599
603static void format_int(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
604 size_t bufsize) {
605 int int_value = 0;
606 if (field_size == sizeof(unsigned short int)) {
607 int_value = *(unsigned short int *)field_ptr;
608 } else {
609 int_value = *(int *)field_ptr;
610 }
611
612 // Emit symbolic names for enum-backed values in generated config.
613 if (meta && meta->field_offset == offsetof(options_t, color_mode)) {
614 const char *name = "auto";
615 if (int_value == COLOR_MODE_NONE) {
616 name = "none";
617 } else if (int_value == COLOR_MODE_16_COLOR) {
618 name = "16";
619 } else if (int_value == COLOR_MODE_256_COLOR) {
620 name = "256";
621 } else if (int_value == COLOR_MODE_TRUECOLOR) {
622 name = "truecolor";
623 }
624 SAFE_SNPRINTF(buf, bufsize, "\"%s\"", name);
625 return;
626 }
627
628 if (meta && meta->field_offset == offsetof(options_t, color_filter)) {
629 const char *name = "none";
630 if (int_value > COLOR_FILTER_NONE && int_value < COLOR_FILTER_COUNT) {
632 if (def && def->cli_name) {
633 name = def->cli_name;
634 }
635 }
636 SAFE_SNPRINTF(buf, bufsize, "\"%s\"", name);
637 return;
638 }
639
640 if (meta && meta->field_offset == offsetof(options_t, render_mode)) {
641 const char *name = "foreground";
642 if (int_value == RENDER_MODE_BACKGROUND) {
643 name = "background";
644 } else if (int_value == RENDER_MODE_HALF_BLOCK) {
645 name = "half-block";
646 }
647 SAFE_SNPRINTF(buf, bufsize, "\"%s\"", name);
648 return;
649 }
650
651 SAFE_SNPRINTF(buf, bufsize, "%d", int_value);
652}
653
657static void format_bool(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
658 size_t bufsize) {
659 bool bool_value = false;
660 if (field_size == sizeof(unsigned short int)) {
661 bool_value = *(unsigned short int *)field_ptr != 0;
662 } else {
663 bool_value = *(bool *)field_ptr;
664 }
665 bool is_inverted_no_splash = false;
666 if (meta && meta->field_offset == offsetof(options_t, splash_screen) && meta->toml_key) {
667 const char *key = meta->toml_key;
668 size_t key_len = strlen(key);
669 static const char suffix[] = ".no_splash";
670 size_t suffix_len = sizeof(suffix) - 1;
671 is_inverted_no_splash = (key_len >= suffix_len && strcmp(key + (key_len - suffix_len), suffix) == 0);
672 }
673 if (is_inverted_no_splash) {
674 bool_value = !bool_value;
675 }
676 SAFE_SNPRINTF(buf, bufsize, "%s", bool_value ? "true" : "false");
677}
678
682static void format_double(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
683 size_t bufsize) {
684 (void)meta;
685
686 if (field_size == sizeof(float)) {
687 float float_value = 0.0f;
688 memcpy(&float_value, field_ptr, sizeof(float));
689 SAFE_SNPRINTF(buf, bufsize, "%.1f", (double)float_value);
690 } else {
691 double double_value = 0.0;
692 memcpy(&double_value, field_ptr, sizeof(double));
693 SAFE_SNPRINTF(buf, bufsize, "%.1f", double_value);
694 }
695}
696
703static void format_callback(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
704 size_t bufsize) {
705 if (!field_ptr || !buf || !meta) {
706 return;
707 }
708
709 if (meta->field_offset == offsetof(options_t, log_file) ||
710 meta->field_offset == offsetof(options_t, palette_custom) ||
711 meta->field_offset == offsetof(options_t, yt_dlp_options)) {
712 format_string(field_ptr, field_size, meta, buf, bufsize);
713 return;
714 }
715
716 if (meta->field_offset == offsetof(options_t, media_seek_timestamp) ||
717 meta->field_offset == offsetof(options_t, microphone_sensitivity) ||
718 meta->field_offset == offsetof(options_t, speakers_volume)) {
719 format_double(field_ptr, field_size, meta, buf, bufsize);
720 return;
721 }
722
723 if (field_size == sizeof(bool)) {
724 format_bool(field_ptr, field_size, meta, buf, bufsize);
725 return;
726 }
727
728 format_int(field_ptr, field_size, meta, buf, bufsize);
729}
730
757static asciichat_error_t config_apply_schema(toml_datum_t toptab, asciichat_mode_t detected_mode, options_t *opts,
758 bool strict) {
759 size_t metadata_count = 0;
760 const config_option_metadata_t *metadata = config_schema_get_all(&metadata_count);
761 asciichat_error_t first_error = ASCIICHAT_OK;
762
763 // Track which options were set to avoid duplicates (e.g., log_file vs logging.log_file)
764 size_t flags_count = metadata_count > 0 ? metadata_count : 1;
765 bool *option_set_flags = SAFE_CALLOC(flags_count, sizeof(bool), bool *);
766 if (!option_set_flags) {
767 asciichat_error_t err = SET_ERRNO(ERROR_MEMORY, "Failed to allocate config option flags");
768 return err;
769 }
770 defer(SAFE_FREE(option_set_flags));
771
772 for (size_t i = 0; i < metadata_count; i++) {
773 const config_option_metadata_t *meta = &metadata[i];
774
775 // Validate mode compatibility using mode_bitmask
776 // If mode_bitmask is 0 or BINARY, option applies to all modes
777 if (meta->mode_bitmask != 0 && !(meta->mode_bitmask & OPTION_MODE_BINARY)) {
778 // Option has specific mode restrictions - check if current mode matches
779 bool applies_to_mode = false;
780 if (detected_mode >= 0 && detected_mode <= MODE_DISCOVERY) {
781 option_mode_bitmask_t mode_bit = (option_mode_bitmask_t)(1 << detected_mode);
782 applies_to_mode = (meta->mode_bitmask & mode_bit) != 0;
783 }
784
785 if (!applies_to_mode) {
786 log_debug("Config: Option '%s' is not supported for this mode (skipping)", meta->toml_key);
787 if (strict) {
788 SAFE_FREE(option_set_flags);
789 return SET_ERRNO(ERROR_CONFIG, "Option '%s' is not supported for this mode", meta->toml_key);
790 }
791 continue;
792 }
793 }
794
795 // Skip if already set (avoid processing duplicates like log_file vs logging.log_file)
796 if (option_set_flags[i]) {
797 continue;
798 }
799
800 // Look up TOML value
801 toml_datum_t datum = toml_seek(toptab, meta->toml_key);
802 if (datum.type == TOML_UNKNOWN) {
803 continue; // Option not present in config
804 }
805
806 // Extract value based on type using handler
807 char value_str[BUFFER_SIZE_MEDIUM] = {0};
808 bool has_value = false;
809 int int_val = 0;
810 bool bool_val = false;
811 double double_val = 0.0;
812
813 // Use type handler for extraction (consolidated from 5 switch cases)
814 // For CALLBACK types, extract manually since they don't have extract handlers
815 if (meta->type == OPTION_TYPE_CALLBACK) {
816 if (datum.type == TOML_STRING) {
817 const char *str = get_toml_string_validated(datum);
818 if (str && strlen(str) > 0) {
819 SAFE_STRNCPY(value_str, str, BUFFER_SIZE_MEDIUM);
820 has_value = true;
821 }
822 } else if (datum.type == TOML_INT64) {
823 // Convert integer to string (e.g., port = 8080)
824 SAFE_SNPRINTF(value_str, BUFFER_SIZE_MEDIUM, "%lld", (long long)datum.u.int64);
825 has_value = true;
826 }
827 } else if (g_type_handlers[meta->type].extract) {
828 g_type_handlers[meta->type].extract(datum, value_str, &int_val, &bool_val, &double_val, &has_value);
829 }
830
831 if (!has_value) {
832 continue;
833 }
834
835 // Special handling for palette-chars (auto-sets palette_type to CUSTOM)
836 // The TOML key is "display.palette_chars" (from DISPLAY group, palette-chars option)
837 if (strcmp(meta->toml_key, "display.palette_chars") == 0) {
838 const char *chars_str = get_toml_string_validated(datum);
839 if (chars_str && strlen(chars_str) > 0) {
840 if (strlen(chars_str) < sizeof(opts->palette_custom)) {
841 SAFE_STRNCPY(opts->palette_custom, chars_str, sizeof(opts->palette_custom));
842 opts->palette_custom[sizeof(opts->palette_custom) - 1] = '\0';
843 opts->palette_custom_set = true;
845 option_set_flags[i] = true;
846 } else {
847 CONFIG_WARN("Invalid palette.chars: too long (%zu chars, max %zu, skipping)", strlen(chars_str),
848 sizeof(opts->palette_custom) - 1);
849 if (strict) {
850 SAFE_FREE(option_set_flags);
851 return SET_ERRNO(ERROR_CONFIG, "palette.chars too long");
852 }
853 }
854 }
855 continue;
856 }
857
858 // Special handling for no-encrypt (auto-disables encrypt_enabled)
859 // The TOML key is "security.no_encrypt" (from SECURITY group, no-encrypt option)
860 if (strcmp(meta->toml_key, "security.no_encrypt") == 0) {
861 bool no_encrypt_val = false;
862 if (strcmp(value_str, "true") == 0 || strcmp(value_str, "1") == 0) {
863 no_encrypt_val = true;
864 } else if (strcmp(value_str, "false") == 0 || strcmp(value_str, "0") == 0) {
865 no_encrypt_val = false;
866 } else {
867 CONFIG_WARN("Invalid no_encrypt value '%s' (expected true/false), skipping", value_str);
868 if (strict) {
869 SAFE_FREE(option_set_flags);
870 return SET_ERRNO(ERROR_CONFIG, "Invalid no_encrypt value");
871 }
872 continue;
873 }
874
875 opts->no_encrypt = no_encrypt_val;
876 if (no_encrypt_val) {
877 // When no_encrypt is enabled, automatically disable encrypt_enabled
878 opts->encrypt_enabled = false;
879 }
880 option_set_flags[i] = true;
881 continue;
882 }
883
884 // Parse and validate value using handler
885 char error_msg[BUFFER_SIZE_SMALL] = {0};
886 option_parsed_value_t parsed = {0};
887 asciichat_error_t parse_result = ASCIICHAT_OK;
888
889 // Handle CALLBACK types using their custom parse function
890 if (meta->type == OPTION_TYPE_CALLBACK) {
891 if (meta->parse_fn) {
892 // Use custom parse function for callbacks (e.g., port parsing)
893 void *field_ptr = (char *)opts + meta->field_offset;
894 char *callback_error = NULL;
895 if (meta->parse_fn(value_str, field_ptr, &callback_error)) {
896 // Successfully parsed - callback has written to opts directly
897 if (callback_error) {
898 SAFE_FREE(callback_error);
899 }
900 // Mark as set to continue to validation
901 option_set_flags[i] = true;
902 // Don't continue - fall through to validation check below
903 } else {
904 // Parsing failed
905 CONFIG_WARN("Invalid %s value '%s': %s (skipping)", meta->toml_key, value_str,
906 callback_error ? callback_error : "parsing failed");
907 if (callback_error) {
908 SAFE_FREE(callback_error);
909 }
910 if (strict) {
911 if (first_error == ASCIICHAT_OK) {
912 first_error = SET_ERRNO(ERROR_CONFIG, "Invalid %s value", meta->toml_key);
913 }
914 }
915 continue;
916 }
917 } else {
918 // No parse function for callback type
919 CONFIG_WARN("No parser for callback %s (parse_fn is NULL) (skipping)", meta->toml_key);
920 continue;
921 }
922 } else if (meta->type == OPTION_TYPE_ACTION) {
923 // Skip action types (not loaded from config)
924 continue;
925 } else if (g_type_handlers[meta->type].parse_validate) {
926 // Use standard type handler for parsing/validation
927 parse_result = g_type_handlers[meta->type].parse_validate(value_str, meta, &parsed, error_msg, sizeof(error_msg));
928 if (parse_result != ASCIICHAT_OK) {
929 CONFIG_WARN("Invalid %s value '%s': %s (skipping)", meta->toml_key, value_str, error_msg);
930 if (strict) {
931 if (first_error == ASCIICHAT_OK) {
932 first_error = SET_ERRNO(ERROR_CONFIG, "Invalid %s: %s", meta->toml_key, error_msg);
933 }
934 }
935 continue;
936 }
937
938 // Write value to options_t using handler
939 if (g_type_handlers[meta->type].write_to_struct) {
940 char error_msg_write[BUFFER_SIZE_SMALL] = {0};
941 asciichat_error_t write_result =
942 g_type_handlers[meta->type].write_to_struct(&parsed, meta, opts, error_msg_write, sizeof(error_msg_write));
943 if (write_result != ASCIICHAT_OK) {
944 CONFIG_WARN("Failed to write %s: %s (skipping)", meta->toml_key, error_msg_write);
945 if (strict) {
946 if (first_error == ASCIICHAT_OK) {
947 first_error = write_result;
948 }
949 }
950 continue;
951 }
952 }
953
954 // Mark option as set
955 option_set_flags[i] = true;
956 } else {
957 // No handler for this type
958 CONFIG_WARN("No handler for %s (skipping)", meta->toml_key);
959 continue;
960 }
961
962 // Call builder's validate function if it exists (for cross-field validation)
963 if (meta->validate_fn) {
964 char *validate_error = NULL;
965 if (!meta->validate_fn(opts, &validate_error)) {
966 CONFIG_WARN("Validation failed for %s: %s (skipping)", meta->toml_key,
967 validate_error ? validate_error : "validation failed");
968 if (validate_error) {
969 SAFE_FREE(validate_error);
970 }
971 if (strict) {
972 if (first_error == ASCIICHAT_OK) {
973 first_error = SET_ERRNO(ERROR_CONFIG, "Validation failed for %s", meta->toml_key);
974 }
975 continue;
976 }
977 continue;
978 }
979 }
980
981 // Mark this option as set
982 option_set_flags[i] = true;
983 }
984
985 // Handle special crypto.no_encrypt logic
986 toml_datum_t no_encrypt = toml_seek(toptab, "crypto.no_encrypt");
987 if (no_encrypt.type == TOML_BOOLEAN && no_encrypt.u.boolean) {
988 opts->no_encrypt = 1;
989 // Don't modify encrypt_enabled - they are independent flags
990 // The conflict validation will catch if both are set
991 }
992
993 // Handle password warning (check both crypto and security sections)
994 toml_datum_t password = toml_seek(toptab, "crypto.password");
995 if (password.type == TOML_UNKNOWN) {
996 password = toml_seek(toptab, "security.password");
997 }
998 if (password.type == TOML_STRING) {
999 const char *password_str = get_toml_string_validated(password);
1000 if (password_str && strlen(password_str) > 0) {
1001 CONFIG_WARN("Password stored in config file is insecure! Use CLI --password instead.");
1002 }
1003 }
1004
1005 SAFE_FREE(option_set_flags);
1006 return first_error;
1007}
1008
1041asciichat_error_t config_load_and_apply(asciichat_mode_t detected_mode, const char *config_path, bool strict,
1042 options_t *opts) {
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}
1180
1184typedef struct {
1185 char *buffer; // Dynamically allocated buffer
1186 size_t size; // Current bytes used
1187 size_t capacity; // Total capacity
1188 bool overflow; // Set if buffer overflows
1190
1197static bool config_builder_append(config_builder_t *builder, const char *fmt, ...) {
1198 if (builder->overflow) {
1199 return false;
1200 }
1201
1202 if (builder->size >= builder->capacity) {
1203 builder->overflow = true;
1204 return false;
1205 }
1206
1207 va_list args;
1208 va_start(args, fmt);
1209 int written = safe_vsnprintf(builder->buffer + builder->size, builder->capacity - builder->size, fmt, args);
1210 va_end(args);
1211
1212 if (written < 0 || builder->size + written >= builder->capacity) {
1213 builder->overflow = true;
1214 return false;
1215 }
1216
1217 builder->size += written;
1218 return true;
1219}
1220
1221static bool config_key_should_be_commented(const char *toml_key) {
1222 if (!toml_key) {
1223 return false;
1224 }
1225
1226 return strcmp(toml_key, "logging.log_file") == 0 || strcmp(toml_key, "security.password") == 0 ||
1227 strcmp(toml_key, "security.key") == 0 || strcmp(toml_key, "security.server_key") == 0 ||
1228 strcmp(toml_key, "security.client_keys") == 0 || strcmp(toml_key, "media.file") == 0 ||
1229 strcmp(toml_key, "media.url") == 0 || strcmp(toml_key, "media.yt_dlp_options") == 0 ||
1230 strcmp(toml_key, "network.turn_secret") == 0;
1231}
1232
1238static asciichat_mode_t extract_mode_from_bitmask(option_mode_bitmask_t mode_bitmask) {
1239 // Check each mode bit
1240 if (mode_bitmask == OPTION_MODE_SERVER)
1241 return MODE_SERVER;
1242 if (mode_bitmask == OPTION_MODE_CLIENT)
1243 return MODE_CLIENT;
1244 if (mode_bitmask == OPTION_MODE_MIRROR)
1245 return MODE_MIRROR;
1246 if (mode_bitmask == OPTION_MODE_DISCOVERY_SVC)
1248 if (mode_bitmask == OPTION_MODE_DISCOVERY)
1249 return MODE_DISCOVERY;
1250 // Bitmask has zero or multiple bits set
1251 return MODE_INVALID;
1252}
1253
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}
1546
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}
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_DOUBLE
Floating point (โ€“ratio 1.5)
Definition builder.h:165
@ OPTION_TYPE_STRING
String value (โ€“name foo)
Definition builder.h:164
@ OPTION_TYPE_ACTION
Action that executes and may exit (โ€“list-webcams, etc.)
Definition builder.h:167
@ OPTION_TYPE_CALLBACK
Custom parser function.
Definition builder.h:166
@ OPTION_TYPE_BOOL
Boolean flag (โ€“flag, no value)
Definition builder.h:162
โš™๏ธ Common definitions, error codes, macros, and types shared throughout the application
#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
TOML configuration file support for ascii-chat.
Defer macro definition for source-to-source transformation.
Cross-platform error handling utilities.
Error and exit codes - unified status values (0-255)
Cross-platform filesystem operations.
#define BUFFER_SIZE_SMALL
Small buffer size (256 bytes)
#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_SNPRINTF(buffer, buffer_size,...)
Definition common.h:492
#define SAFE_CALLOC(count, size, cast)
Definition common.h:274
#define SAFE_FCLOSE(fp)
Definition common.h:386
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)
Definition config.c:1547
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
asciichat_error_t config_create_default(const char *config_path)
Create default configuration file with all default values.
Definition config.c:1254
#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.
#define CLEAR_ERRNO()
Clear the current error state.
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
@ ERROR_INVALID_PARAM
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
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
#define log_file(...)
File-only logging - writes to log file only, no stderr output.
Definition log/log.h:646
#define log_plain(...)
Plain logging - writes to both log file and stderr without timestamps or log levels.
Definition log/log.h:618
#define COLOR_MODE_16_COLOR
16-color mode (full name)
#define COLOR_MODE_256_COLOR
256-color mode (full name)
#define COLOR_MODE_TRUECOLOR
24-bit truecolor mode
int validate_opt_render_mode(const char *value_str, char *error_msg, size_t error_msg_size)
Validate render mode string.
Definition validation.c:172
asciichat_mode_t
Mode type for options parsing.
#define COLOR_MODE_AUTO
Backward compatibility aliases for color mode enum values.
int validate_opt_color_mode(const char *value_str, char *error_msg, size_t error_msg_size)
Validate color mode string.
Definition validation.c:138
int validate_opt_palette(const char *value_str, char *error_msg, size_t error_msg_size)
Validate palette type string.
Definition validation.c:200
#define OPTIONS_BUFF_SIZE
Buffer size for option string values.
option_mode_bitmask_t
Option mode bitmask.
#define COLOR_MODE_NONE
Monochrome mode.
options_t options_t_new(void)
Initialize options by parsing command-line arguments.
@ MODE_CLIENT
Client mode - network client options.
@ MODE_DISCOVERY_SERVICE
Discovery server mode - session management and WebRTC signaling.
@ MODE_INVALID
Invalid mode.
@ MODE_SERVER
Server mode - network server options.
@ MODE_MIRROR
Mirror mode - local webcam viewing (no network)
@ MODE_DISCOVERY
Discovery mode - participant that can dynamically become host.
@ OPTION_MODE_BINARY
Binary-level options (parsed before mode detection)
@ OPTION_MODE_CLIENT
Client mode (bit 1)
@ OPTION_MODE_DISCOVERY
Discovery mode (bit 4)
@ OPTION_MODE_SERVER
Server mode (bit 0)
@ OPTION_MODE_MIRROR
Mirror mode (bit 2)
@ OPTION_MODE_DISCOVERY_SVC
Discovery server mode (bit 3)
@ PALETTE_COUNT
Number of palette types (not a valid palette)
Definition palette.h:100
@ PALETTE_CUSTOM
User-defined via โ€“palette-chars.
Definition palette.h:98
@ PALETTE_STANDARD
Standard ASCII palette: " ...',;:clodxkO0KXNWM".
Definition palette.h:88
#define DIR_PERM_PRIVATE
Directory permission: Private (owner read/write/execute only)
Definition filesystem.h:199
asciichat_error_t platform_find_config_file(const char *filename, config_file_list_t *list_out)
Find config file across multiple standard locations.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
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
int safe_vsnprintf(char *buffer, size_t buffer_size, const char *format, va_list ap)
Safe formatted string printing with va_list.
Definition system.c:199
#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)
int errno
void config_file_list_destroy(config_file_list_t *list)
Free config file list resources.
bool path_looks_like_path(const char *value)
Determine if a string is likely intended to reference the filesystem.
Definition path.c:809
bool utf8_is_valid(const char *str)
Check if a string is valid UTF-8.
Definition utf8.c:156
path_role_t
Classification for user-supplied filesystem paths.
Definition path.h:316
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
char * get_config_dir(void)
Get configuration directory path with XDG_CONFIG_HOME support.
Definition path.c:527
@ PATH_ROLE_CONFIG_FILE
Definition path.h:317
@ PATH_ROLE_KEY_PUBLIC
Definition path.h:320
@ PATH_ROLE_LOG_FILE
Definition path.h:318
@ PATH_ROLE_KEY_PRIVATE
Definition path.h:319
color_filter_t color_filter_from_cli_name(const char *cli_name)
Convert color filter CLI name to enum value.
const color_filter_def_t * color_filter_get_metadata(color_filter_t filter)
Get color filter metadata by enum value.
โš™๏ธ Unified options parsing system for ascii-chat with builder pattern and lock-free access
๐Ÿ“ Logging API with multiple log levels and terminal output control
action_args_t args
Validation functions for options parsing.
ASCII Palette Management for Video-to-ASCII Conversion.
๐Ÿ“‚ Path Manipulation Utilities
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
Cross-platform interactive prompting utilities.
asciichat_error_t options_state_set(const options_t *opts)
Set options from a parsed options struct.
Definition rcu.c:439
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
Schema metadata for config file options.
Cross-platform stat and file type checking macros.
Color filter metadata structure.
const char * cli_name
CLI argument name (e.g., "matrix-green")
Helper structure for building config content in a buffer.
Definition config.c:1184
size_t capacity
Definition config.c:1187
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
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
bool(* parse_fn)(const char *arg, void *dest, char **error_msg)
Custom parser for callbacks (or NULL)
Definition schema.h:62
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
union config_option_metadata_t::@20 constraints
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
builder_validate_fn_t validate_fn
Builder's validation function (can be NULL for simple types)
Definition schema.h:59
const char * toml_key
TOML key path (e.g., "network.port", "client.address")
Definition schema.h:50
struct config_option_metadata_t::@20::@21 int_range
Type handler - encapsulates all 4 operations for one option type.
Definition config.c:145
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
void(* extract)(toml_datum_t datum, char *value_str, int *int_val, bool *bool_val, double *double_val, bool *has_value)
Extract value from TOML datum.
Definition config.c:146
asciichat_error_t(* parse_validate)(const char *value_str, const config_option_metadata_t *meta, option_parsed_value_t *parsed, char *error_msg, size_t error_size)
Parse and validate value.
Definition config.c:148
asciichat_error_t(* write_to_struct)(const option_parsed_value_t *parsed, const config_option_metadata_t *meta, options_t *opts, char *error_msg, size_t error_size)
Write to options struct.
Definition config.c:151
Options configuration.
Definition builder.h:401
Consolidated options structure.
bool encrypt_enabled
Enable encryption.
bool no_encrypt
Disable encryption (opt-out)
char palette_custom[256]
Custom palette characters.
bool palette_custom_set
True if custom palette was set.
palette_type_t palette_type
Selected palette type.
Cross-platform system functions interface for ascii-chat.
๐Ÿ–ฅ๏ธ Cross-platform terminal interface for ascii-chat
color_filter_t
Monochromatic color filter enumeration.
Definition terminal.h:599
@ COLOR_FILTER_NONE
No filtering (default)
Definition terminal.h:601
@ COLOR_FILTER_COUNT
Total count of filters (not a valid filter)
Definition terminal.h:627
@ RENDER_MODE_FOREGROUND
Foreground colors only (text color)
Definition terminal.h:664
@ RENDER_MODE_BACKGROUND
Background colors (block colors)
Definition terminal.h:666
@ RENDER_MODE_HALF_BLOCK
Unicode half-block characters (mixed foreground/background)
Definition terminal.h:668
Union holding all possible parsed option values.
Definition config.c:121
char str_value[OPTIONS_BUFF_SIZE]
Definition config.c:122
๐Ÿ”ค UTF-8 Encoding and Decoding Utilities
#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