ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
config.c
Go to the documentation of this file.
1
7#include <ascii-chat/options/config.h>
8#include <ascii-chat/common/error_codes.h>
9#include <ascii-chat/options/options.h>
10#include <ascii-chat/options/validation.h>
11#include <ascii-chat/options/schema.h>
12#include <ascii-chat/options/rcu.h>
13#include <ascii-chat/util/path.h>
14#include <ascii-chat/util/utf8.h>
15#include <ascii-chat/common.h>
16#include <ascii-chat/platform/terminal.h>
17#include <ascii-chat/platform/system.h>
18#include <ascii-chat/platform/question.h>
19#include <ascii-chat/platform/stat.h>
20#include <ascii-chat/platform/filesystem.h>
21#include <ascii-chat/crypto/crypto.h>
22#include <ascii-chat/log/logging.h>
23#include <ascii-chat/video/palette.h>
24#include <ascii-chat/video/color_filter.h>
25#include <ascii-chat/version.h>
26#include <ascii-chat/tooling/defer/defer.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 if (value_str && (strcmp(value_str, "true") == 0 || strcmp(value_str, "1") == 0 || strcmp(value_str, "yes") == 0)) {
415 parsed->bool_value = true;
416 } else {
417 parsed->bool_value = false;
418 }
419 return ASCIICHAT_OK;
420}
421
425static asciichat_error_t parse_validate_double(const char *value_str, const config_option_metadata_t *meta,
426 option_parsed_value_t *parsed, char *error_msg, size_t error_size) {
427 char *endptr = NULL;
428 double parsed_val = strtod(value_str, &endptr);
429 if (*endptr != '\0') {
430 SAFE_SNPRINTF(error_msg, error_size, "Invalid float: %s", value_str);
431 return ERROR_CONFIG;
432 }
433
434 // Check numeric range constraints if defined (for double types)
435 if (meta && meta->constraints.int_range.max != 0) {
436 // Use int_range for doubles too (the values are stored as doubles in the constraint)
437 double min = (double)meta->constraints.int_range.min;
438 double max = (double)meta->constraints.int_range.max;
439 if (parsed_val < min || parsed_val > max) {
440 SAFE_SNPRINTF(error_msg, error_size, "Float %.2f out of range [%.2f-%.2f]: %s", parsed_val, min, max, value_str);
441 return ERROR_CONFIG;
442 }
443 }
444
445 parsed->float_value = (float)parsed_val;
446 return ASCIICHAT_OK;
447}
448
452static asciichat_error_t write_string(const option_parsed_value_t *parsed, const config_option_metadata_t *meta,
453 options_t *opts, char *error_msg, size_t error_size) {
454 (void)error_msg;
455 (void)error_size;
456
457 char *field_ptr = ((char *)opts) + meta->field_offset;
458 const char *final_value = parsed->str_value;
459
460 // Special handling for path-based options (keys, log files)
461 bool is_path_option = (strstr(meta->toml_key, "key") != NULL || strstr(meta->toml_key, "log_file") != NULL ||
462 strstr(meta->toml_key, "keyfile") != NULL);
463 if (is_path_option) {
464 // Check if it's a path that needs normalization
465 if (path_looks_like_path(final_value)) {
466 char *normalized = NULL;
467 path_role_t role = PATH_ROLE_CONFIG_FILE; // Default
468 if (strstr(meta->toml_key, "key") != NULL) {
469 role = (strstr(meta->toml_key, "server_key") != NULL || strstr(meta->toml_key, "client_keys") != NULL)
470 ? PATH_ROLE_KEY_PUBLIC
471 : PATH_ROLE_KEY_PRIVATE;
472 } else if (strstr(meta->toml_key, "log_file") != NULL) {
473 role = PATH_ROLE_LOG_FILE;
474 }
475
476 asciichat_error_t path_result = path_validate_user_path(final_value, role, &normalized);
477 if (path_result != ASCIICHAT_OK) {
478 SAFE_FREE(normalized);
479 return path_result;
480 }
481 SAFE_STRNCPY(field_ptr, normalized, meta->field_size);
482 SAFE_FREE(normalized);
483 } else {
484 // Not a path, just an identifier (e.g., "gpg:keyid", "github:user")
485 SAFE_STRNCPY(field_ptr, final_value, meta->field_size);
486 }
487
488 // Auto-enable encryption for crypto.key, crypto.password, crypto.keyfile
489 if (strstr(meta->toml_key, "crypto.key") != NULL || strstr(meta->toml_key, "crypto.password") != NULL ||
490 strstr(meta->toml_key, "crypto.keyfile") != NULL) {
491 opts->encrypt_enabled = 1;
492 }
493 } else {
494 SAFE_STRNCPY(field_ptr, final_value, meta->field_size);
495 }
496
497 return ASCIICHAT_OK;
498}
499
503static asciichat_error_t write_int(const option_parsed_value_t *parsed, const config_option_metadata_t *meta,
504 options_t *opts, char *error_msg, size_t error_size) {
505 (void)error_msg;
506 (void)error_size;
507
508 char *field_ptr = ((char *)opts) + meta->field_offset;
509
510 // Handle unsigned short int fields (webcam_index)
511 if (meta->field_size == sizeof(unsigned short int)) {
512 *(unsigned short int *)field_ptr = (unsigned short int)parsed->int_value;
513 } else {
514 *(int *)field_ptr = parsed->int_value;
515 }
516
517 return ASCIICHAT_OK;
518}
519
523static asciichat_error_t write_bool(const option_parsed_value_t *parsed, const config_option_metadata_t *meta,
524 options_t *opts, char *error_msg, size_t error_size) {
525 (void)error_msg;
526 (void)error_size;
527
528 char *field_ptr = ((char *)opts) + meta->field_offset;
529 bool bool_value = parsed->bool_value;
530 bool is_inverted_no_splash = false;
531 if (meta && meta->field_offset == offsetof(options_t, splash) && meta->toml_key) {
532 const char *key = meta->toml_key;
533 size_t key_len = strlen(key);
534 static const char suffix[] = ".no_splash";
535 size_t suffix_len = sizeof(suffix) - 1;
536 is_inverted_no_splash = (key_len >= suffix_len && strcmp(key + (key_len - suffix_len), suffix) == 0);
537 }
538 if (is_inverted_no_splash) {
539 bool_value = !bool_value;
540 }
541
542 // Handle unsigned short int bool fields (common in options_t)
543 if (meta->field_size == sizeof(unsigned short int)) {
544 *(unsigned short int *)field_ptr = bool_value ? 1 : 0;
545 } else {
546 *(bool *)field_ptr = bool_value;
547 }
548
549 return ASCIICHAT_OK;
550}
551
555static asciichat_error_t write_double(const option_parsed_value_t *parsed, const config_option_metadata_t *meta,
556 options_t *opts, char *error_msg, size_t error_size) {
557 (void)error_msg;
558 (void)error_size;
559
560 char *field_ptr = ((char *)opts) + meta->field_offset;
561
562 // Check field_size to distinguish float from double
563 // Use memcpy for alignment-safe writes
564 if (meta->field_size == sizeof(float)) {
565 float float_val = (float)parsed->float_value;
566 memcpy(field_ptr, &float_val, sizeof(float));
567 } else {
568 double double_val = (double)parsed->float_value;
569 memcpy(field_ptr, &double_val, sizeof(double));
570 }
571
572 return ASCIICHAT_OK;
573}
574
578static void format_string(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
579 size_t bufsize) {
580 (void)field_size;
581 (void)meta;
582
583 const char *str_value = (const char *)field_ptr;
584 if (str_value && strlen(str_value) > 0) {
585 char escaped_str[BUFFER_SIZE_MEDIUM * 2]; // Max 2x size for escaping '%'
586 size_t j = 0;
587 for (size_t i = 0; i < strlen(str_value) && j < sizeof(escaped_str) - 2; ++i) {
588 if (str_value[i] == '%') {
589 escaped_str[j++] = '%';
590 escaped_str[j++] = '%';
591 } else {
592 escaped_str[j++] = str_value[i];
593 }
594 }
595 escaped_str[j] = '\0';
596
597 SAFE_SNPRINTF(buf, bufsize, "\"%s\"", escaped_str);
598 } else {
599 SAFE_SNPRINTF(buf, bufsize, "\"\"");
600 }
601}
602
606static void format_int(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
607 size_t bufsize) {
608 int int_value = 0;
609 if (field_size == sizeof(unsigned short int)) {
610 int_value = *(unsigned short int *)field_ptr;
611 } else {
612 int_value = *(int *)field_ptr;
613 }
614
615 // Emit symbolic names for enum-backed values in generated config.
616 if (meta && meta->field_offset == offsetof(options_t, color_mode)) {
617 const char *name = "auto";
618 if (int_value == COLOR_MODE_NONE) {
619 name = "none";
620 } else if (int_value == COLOR_MODE_16_COLOR) {
621 name = "16";
622 } else if (int_value == COLOR_MODE_256_COLOR) {
623 name = "256";
624 } else if (int_value == COLOR_MODE_TRUECOLOR) {
625 name = "truecolor";
626 }
627 SAFE_SNPRINTF(buf, bufsize, "\"%s\"", name);
628 return;
629 }
630
631 if (meta && meta->field_offset == offsetof(options_t, color_filter)) {
632 const char *name = "none";
633 if (int_value > COLOR_FILTER_NONE && int_value < COLOR_FILTER_COUNT) {
634 const color_filter_def_t *def = color_filter_get_metadata((color_filter_t)int_value);
635 if (def && def->cli_name) {
636 name = def->cli_name;
637 }
638 }
639 SAFE_SNPRINTF(buf, bufsize, "\"%s\"", name);
640 return;
641 }
642
643 if (meta && meta->field_offset == offsetof(options_t, render_mode)) {
644 const char *name = "foreground";
645 if (int_value == RENDER_MODE_BACKGROUND) {
646 name = "background";
647 } else if (int_value == RENDER_MODE_HALF_BLOCK) {
648 name = "half-block";
649 }
650 SAFE_SNPRINTF(buf, bufsize, "\"%s\"", name);
651 return;
652 }
653
654 SAFE_SNPRINTF(buf, bufsize, "%d", int_value);
655}
656
660static void format_bool(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
661 size_t bufsize) {
662 bool bool_value = false;
663 if (field_size == sizeof(unsigned short int)) {
664 bool_value = *(unsigned short int *)field_ptr != 0;
665 } else {
666 bool_value = *(bool *)field_ptr;
667 }
668 bool is_inverted_no_splash = false;
669 if (meta && meta->field_offset == offsetof(options_t, splash) && meta->toml_key) {
670 const char *key = meta->toml_key;
671 size_t key_len = strlen(key);
672 static const char suffix[] = ".no_splash";
673 size_t suffix_len = sizeof(suffix) - 1;
674 is_inverted_no_splash = (key_len >= suffix_len && strcmp(key + (key_len - suffix_len), suffix) == 0);
675 }
676 if (is_inverted_no_splash) {
677 bool_value = !bool_value;
678 }
679 SAFE_SNPRINTF(buf, bufsize, "%s", bool_value ? "true" : "false");
680}
681
685static void format_double(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
686 size_t bufsize) {
687 (void)meta;
688
689 if (field_size == sizeof(float)) {
690 float float_value = 0.0f;
691 memcpy(&float_value, field_ptr, sizeof(float));
692 SAFE_SNPRINTF(buf, bufsize, "%.1f", (double)float_value);
693 } else {
694 double double_value = 0.0;
695 memcpy(&double_value, field_ptr, sizeof(double));
696 SAFE_SNPRINTF(buf, bufsize, "%.1f", double_value);
697 }
698}
699
706static void format_callback(const char *field_ptr, size_t field_size, const config_option_metadata_t *meta, char *buf,
707 size_t bufsize) {
708 if (!field_ptr || !buf || !meta) {
709 return;
710 }
711
712 if (meta->field_offset == offsetof(options_t, log_file) ||
713 meta->field_offset == offsetof(options_t, palette_custom) ||
714 meta->field_offset == offsetof(options_t, yt_dlp_options)) {
715 format_string(field_ptr, field_size, meta, buf, bufsize);
716 return;
717 }
718
719 if (meta->field_offset == offsetof(options_t, media_seek_timestamp) ||
720 meta->field_offset == offsetof(options_t, microphone_sensitivity) ||
721 meta->field_offset == offsetof(options_t, speakers_volume)) {
722 format_double(field_ptr, field_size, meta, buf, bufsize);
723 return;
724 }
725
726 if (field_size == sizeof(bool)) {
727 format_bool(field_ptr, field_size, meta, buf, bufsize);
728 return;
729 }
730
731 format_int(field_ptr, field_size, meta, buf, bufsize);
732}
733
760static asciichat_error_t config_apply_schema(toml_datum_t toptab, asciichat_mode_t detected_mode, options_t *opts,
761 bool strict) {
762 size_t metadata_count = 0;
763 const config_option_metadata_t *metadata = config_schema_get_all(&metadata_count);
764 asciichat_error_t first_error = ASCIICHAT_OK;
765
766 // Track which options were set to avoid duplicates (e.g., log_file vs logging.log_file)
767 size_t flags_count = metadata_count > 0 ? metadata_count : 1;
768 bool *option_set_flags = SAFE_CALLOC(flags_count, sizeof(bool), bool *);
769 if (!option_set_flags) {
770 asciichat_error_t err = SET_ERRNO(ERROR_MEMORY, "Failed to allocate config option flags");
771 return err;
772 }
773 defer(SAFE_FREE(option_set_flags));
774
775 for (size_t i = 0; i < metadata_count; i++) {
776 const config_option_metadata_t *meta = &metadata[i];
777
778 // Validate mode compatibility using mode_bitmask
779 // If mode_bitmask is 0 or BINARY, option applies to all modes
780 if (meta->mode_bitmask != 0 && !(meta->mode_bitmask & OPTION_MODE_BINARY)) {
781 // Option has specific mode restrictions - check if current mode matches
782 bool applies_to_mode = false;
783 if (detected_mode >= 0 && detected_mode <= MODE_DISCOVERY) {
784 option_mode_bitmask_t mode_bit = (1 << detected_mode);
785 applies_to_mode = (meta->mode_bitmask & mode_bit) != 0;
786 }
787
788 if (!applies_to_mode) {
789 log_debug("Config: Option '%s' is not supported for this mode (skipping)", meta->toml_key);
790 if (strict) {
791 SAFE_FREE(option_set_flags);
792 return SET_ERRNO(ERROR_CONFIG, "Option '%s' is not supported for this mode", meta->toml_key);
793 }
794 continue;
795 }
796 }
797
798 // Skip if already set (avoid processing duplicates like log_file vs logging.log_file)
799 if (option_set_flags[i]) {
800 continue;
801 }
802
803 // Look up TOML value
804 toml_datum_t datum = toml_seek(toptab, meta->toml_key);
805 if (datum.type == TOML_UNKNOWN) {
806 continue; // Option not present in config
807 }
808
809 // Extract value based on type using handler
810 char value_str[BUFFER_SIZE_MEDIUM] = {0};
811 bool has_value = false;
812 int int_val = 0;
813 bool bool_val = false;
814 double double_val = 0.0;
815
816 // Use type handler for extraction (consolidated from 5 switch cases)
817 // For CALLBACK types, extract manually since they don't have extract handlers
818 if (meta->type == OPTION_TYPE_CALLBACK) {
819 if (datum.type == TOML_STRING) {
820 const char *str = get_toml_string_validated(datum);
821 if (str && strlen(str) > 0) {
822 SAFE_STRNCPY(value_str, str, BUFFER_SIZE_MEDIUM);
823 has_value = true;
824 }
825 } else if (datum.type == TOML_INT64) {
826 // Convert integer to string (e.g., port = 8080)
827 SAFE_SNPRINTF(value_str, BUFFER_SIZE_MEDIUM, "%lld", (long long)datum.u.int64);
828 has_value = true;
829 }
830 } else if (g_type_handlers[meta->type].extract) {
831 g_type_handlers[meta->type].extract(datum, value_str, &int_val, &bool_val, &double_val, &has_value);
832 }
833
834 if (!has_value) {
835 continue;
836 }
837
838 // Special handling for palette-chars (auto-sets palette_type to CUSTOM)
839 // The TOML key is "display.palette_chars" (from DISPLAY group, palette-chars option)
840 if (strcmp(meta->toml_key, "display.palette_chars") == 0) {
841 const char *chars_str = get_toml_string_validated(datum);
842 if (chars_str && strlen(chars_str) > 0) {
843 if (strlen(chars_str) < sizeof(opts->palette_custom)) {
844 SAFE_STRNCPY(opts->palette_custom, chars_str, sizeof(opts->palette_custom));
845 opts->palette_custom[sizeof(opts->palette_custom) - 1] = '\0';
846 opts->palette_custom_set = true;
847 opts->palette_type = PALETTE_CUSTOM;
848 option_set_flags[i] = true;
849 } else {
850 CONFIG_WARN("Invalid palette.chars: too long (%zu chars, max %zu, skipping)", strlen(chars_str),
851 sizeof(opts->palette_custom) - 1);
852 if (strict) {
853 SAFE_FREE(option_set_flags);
854 return SET_ERRNO(ERROR_CONFIG, "palette.chars too long");
855 }
856 }
857 }
858 continue;
859 }
860
861 // Special handling for no-encrypt (auto-disables encrypt_enabled)
862 // The TOML key is "security.no_encrypt" (from SECURITY group, no-encrypt option)
863 if (strcmp(meta->toml_key, "security.no_encrypt") == 0) {
864 bool no_encrypt_val = false;
865 if (strcmp(value_str, "true") == 0 || strcmp(value_str, "1") == 0) {
866 no_encrypt_val = true;
867 } else if (strcmp(value_str, "false") == 0 || strcmp(value_str, "0") == 0) {
868 no_encrypt_val = false;
869 } else {
870 CONFIG_WARN("Invalid no_encrypt value '%s' (expected true/false), skipping", value_str);
871 if (strict) {
872 SAFE_FREE(option_set_flags);
873 return SET_ERRNO(ERROR_CONFIG, "Invalid no_encrypt value");
874 }
875 continue;
876 }
877
878 opts->no_encrypt = no_encrypt_val;
879 if (no_encrypt_val) {
880 // When no_encrypt is enabled, automatically disable encrypt_enabled
881 opts->encrypt_enabled = false;
882 }
883 option_set_flags[i] = true;
884 continue;
885 }
886
887 // Parse and validate value using handler
888 char error_msg[BUFFER_SIZE_SMALL] = {0};
889 option_parsed_value_t parsed = {0};
890 asciichat_error_t parse_result = ASCIICHAT_OK;
891
892 // Handle CALLBACK types using their custom parse function
893 if (meta->type == OPTION_TYPE_CALLBACK) {
894 if (meta->parse_fn) {
895 // Use custom parse function for callbacks (e.g., port parsing)
896 void *field_ptr = (char *)opts + meta->field_offset;
897 char *callback_error = NULL;
898 if (meta->parse_fn(value_str, field_ptr, &callback_error)) {
899 // Successfully parsed - callback has written to opts directly
900 if (callback_error) {
901 SAFE_FREE(callback_error);
902 }
903 // Mark as set to continue to validation
904 option_set_flags[i] = true;
905 // Don't continue - fall through to validation check below
906 } else {
907 // Parsing failed
908 CONFIG_WARN("Invalid %s value '%s': %s (skipping)", meta->toml_key, value_str,
909 callback_error ? callback_error : "parsing failed");
910 if (callback_error) {
911 SAFE_FREE(callback_error);
912 }
913 if (strict) {
914 if (first_error == ASCIICHAT_OK) {
915 first_error = SET_ERRNO(ERROR_CONFIG, "Invalid %s value", meta->toml_key);
916 }
917 }
918 continue;
919 }
920 } else {
921 // No parse function for callback type
922 CONFIG_WARN("No parser for callback %s (parse_fn is NULL) (skipping)", meta->toml_key);
923 continue;
924 }
925 } else if (meta->type == OPTION_TYPE_ACTION) {
926 // Skip action types (not loaded from config)
927 continue;
928 } else if (g_type_handlers[meta->type].parse_validate) {
929 // Use standard type handler for parsing/validation
930 parse_result = g_type_handlers[meta->type].parse_validate(value_str, meta, &parsed, error_msg, sizeof(error_msg));
931 if (parse_result != ASCIICHAT_OK) {
932 CONFIG_WARN("Invalid %s value '%s': %s (skipping)", meta->toml_key, value_str, error_msg);
933 if (strict) {
934 if (first_error == ASCIICHAT_OK) {
935 first_error = SET_ERRNO(ERROR_CONFIG, "Invalid %s: %s", meta->toml_key, error_msg);
936 }
937 }
938 continue;
939 }
940
941 // Write value to options_t using handler
942 if (g_type_handlers[meta->type].write_to_struct) {
943 char error_msg_write[BUFFER_SIZE_SMALL] = {0};
944 asciichat_error_t write_result =
945 g_type_handlers[meta->type].write_to_struct(&parsed, meta, opts, error_msg_write, sizeof(error_msg_write));
946 if (write_result != ASCIICHAT_OK) {
947 CONFIG_WARN("Failed to write %s: %s (skipping)", meta->toml_key, error_msg_write);
948 if (strict) {
949 if (first_error == ASCIICHAT_OK) {
950 first_error = write_result;
951 }
952 }
953 continue;
954 }
955 }
956
957 // Mark option as set
958 option_set_flags[i] = true;
959 } else {
960 // No handler for this type
961 CONFIG_WARN("No handler for %s (skipping)", meta->toml_key);
962 continue;
963 }
964
965 // Call builder's validate function if it exists (for cross-field validation)
966 if (meta->validate_fn) {
967 char *validate_error = NULL;
968 if (!meta->validate_fn(opts, &validate_error)) {
969 CONFIG_WARN("Validation failed for %s: %s (skipping)", meta->toml_key,
970 validate_error ? validate_error : "validation failed");
971 if (validate_error) {
972 SAFE_FREE(validate_error);
973 }
974 if (strict) {
975 if (first_error == ASCIICHAT_OK) {
976 first_error = SET_ERRNO(ERROR_CONFIG, "Validation failed for %s", meta->toml_key);
977 }
978 continue;
979 }
980 continue;
981 }
982 }
983
984 // Mark this option as set
985 option_set_flags[i] = true;
986 }
987
988 // Handle special crypto.no_encrypt logic
989 toml_datum_t no_encrypt = toml_seek(toptab, "crypto.no_encrypt");
990 if (no_encrypt.type == TOML_BOOLEAN && no_encrypt.u.boolean) {
991 opts->no_encrypt = 1;
992 // Don't modify encrypt_enabled - they are independent flags
993 // The conflict validation will catch if both are set
994 }
995
996 // Handle password warning (check both crypto and security sections)
997 toml_datum_t password = toml_seek(toptab, "crypto.password");
998 if (password.type == TOML_UNKNOWN) {
999 password = toml_seek(toptab, "security.password");
1000 }
1001 if (password.type == TOML_STRING) {
1002 const char *password_str = get_toml_string_validated(password);
1003 if (password_str && strlen(password_str) > 0) {
1004 CONFIG_WARN("Password stored in config file is insecure! Use CLI --password instead.");
1005 }
1006 }
1007
1008 SAFE_FREE(option_set_flags);
1009 return first_error;
1010}
1011
1044asciichat_error_t config_load_and_apply(asciichat_mode_t detected_mode, const char *config_path, bool strict,
1045 options_t *opts) {
1046 // detected_mode is used in config_apply_schema for bitmask validation
1047 char *config_path_expanded = NULL;
1048 defer(SAFE_FREE(config_path_expanded));
1049
1050 if (config_path) {
1051 // Use custom path provided
1052 config_path_expanded = expand_path(config_path);
1053 if (!config_path_expanded) {
1054 // If expansion fails, try using as-is (might already be absolute)
1055 config_path_expanded = platform_strdup(config_path);
1056 }
1057 } else {
1058 // Use default location with XDG support
1059 char *config_dir = get_config_dir();
1060 defer(SAFE_FREE(config_dir));
1061 if (config_dir) {
1062 size_t len = strlen(config_dir) + strlen("config.toml") + 1;
1063 config_path_expanded = SAFE_MALLOC(len, char *);
1064 if (config_path_expanded) {
1065 safe_snprintf(config_path_expanded, len, "%sconfig.toml", config_dir);
1066 }
1067 }
1068
1069 // Fallback to ~/.ascii-chat/config.toml
1070 if (!config_path_expanded) {
1071 config_path_expanded = expand_path("~/.ascii-chat/config.toml");
1072 }
1073 }
1074
1075 if (!config_path_expanded) {
1076 if (strict) {
1077 return SET_ERRNO(ERROR_CONFIG, "Failed to resolve config file path");
1078 }
1079 return ASCIICHAT_OK;
1080 }
1081
1082 char *validated_config_path = NULL;
1083 asciichat_error_t validate_result =
1084 path_validate_user_path(config_path_expanded, PATH_ROLE_CONFIG_FILE, &validated_config_path);
1085 if (validate_result != ASCIICHAT_OK) {
1086 SAFE_FREE(validated_config_path);
1087 SAFE_FREE(config_path_expanded);
1088 return validate_result;
1089 }
1090 // Free the old path before reassigning (defer will free the new one)
1091 if (config_path_expanded != validated_config_path) {
1092 SAFE_FREE(config_path_expanded);
1093 }
1094 config_path_expanded = validated_config_path;
1095
1096 // Determine display path for error messages (before any early returns)
1097 const char *display_path = config_path ? config_path : config_path_expanded;
1098
1099 // Log that we're attempting to load config (before logging is initialized, use stderr)
1100 // Only print if terminal output is enabled (suppress with --quiet)
1101 if (config_path && log_get_terminal_output()) {
1102 log_debug("Loading configuration from: %s", display_path);
1103 }
1104
1105 // Check if config file exists
1106 struct stat st;
1107 if (stat(config_path_expanded, &st) != 0) {
1108 if (strict) {
1109 return SET_ERRNO(ERROR_CONFIG, "Config file does not exist: '%s'", display_path);
1110 }
1111 // File doesn't exist, that's OK - not required (non-strict mode)
1112 return ASCIICHAT_OK;
1113 }
1114
1115 // Verify it's a regular file
1116 if (!S_ISREG(st.st_mode)) {
1117 if (strict) {
1118 return SET_ERRNO(ERROR_CONFIG, "Config file exists but is not a regular file: '%s'", display_path);
1119 }
1120 CONFIG_WARN("Config file exists but is not a regular file: '%s' (skipping)", display_path);
1121 return ASCIICHAT_OK;
1122 }
1123
1124 // Parse TOML file
1125 toml_result_t result = toml_parse_file_ex(config_path_expanded);
1126 // Ensure TOML resources are freed at ALL function exit points (defer handles cleanup)
1127 defer(toml_free(result));
1128
1129 if (!result.ok) {
1130 // result.errmsg is an array, so check its first character
1131 const char *errmsg = (strlen(result.errmsg) > 0) ? result.errmsg : "Unknown parse error";
1132
1133 if (strict) {
1134 // For strict mode, return detailed error message directly
1135 // Note: SET_ERRNO stores the message in context, but asciichat_error_string() only returns generic codes
1136 // So we need to format the error message ourselves here
1137 char error_buffer[BUFFER_SIZE_MEDIUM];
1138 safe_snprintf(error_buffer, sizeof(error_buffer), "Failed to parse config file '%s': %s", display_path, errmsg);
1139 toml_free(result); // Explicit cleanup before return (defer transformation not applied)
1140 return SET_ERRNO(ERROR_CONFIG, "%s", error_buffer);
1141 }
1142 CONFIG_WARN("Failed to parse config file '%s': %s (skipping)", display_path, errmsg);
1143 toml_free(result); // Explicit cleanup before return (defer transformation not applied)
1144 return ASCIICHAT_OK; // Non-fatal error
1145 }
1146
1147 // Apply configuration using schema-driven parser with bitmask validation
1148 asciichat_error_t schema_result = config_apply_schema(result.toptab, detected_mode, opts, strict);
1149
1150 if (schema_result != ASCIICHAT_OK && strict) {
1151 toml_free(result); // Explicit cleanup before return (defer transformation not applied)
1152 return schema_result;
1153 }
1154 // In non-strict mode, continue even if some options failed validation
1155
1156 CONFIG_DEBUG("Loaded configuration from %s", display_path);
1157
1158 // Log successful config load (use stderr since logging may not be initialized yet)
1159 // Only print if terminal output is enabled (suppress with --quiet)
1161 log_debug("Loaded configuration from: %s", display_path);
1162 }
1163
1164 // Update RCU system with modified options (for test compatibility)
1165 // In real usage, options_state_set is called later after CLI parsing
1166 asciichat_error_t rcu_result = options_state_set(opts);
1167 if (rcu_result != ASCIICHAT_OK) {
1168 // Non-fatal - RCU might not be initialized yet in some test scenarios
1169 // But log as warning so tests can see if this is the issue
1170 CONFIG_WARN("Failed to update RCU options state: %d (values may not be persisted)", rcu_result);
1171 }
1172
1173 toml_free(result); // Explicit cleanup before return (defer transformation not applied)
1174 return ASCIICHAT_OK;
1175}
1176
1180typedef struct {
1181 char *buffer; // Dynamically allocated buffer
1182 size_t size; // Current bytes used
1183 size_t capacity; // Total capacity
1184 bool overflow; // Set if buffer overflows
1186
1193static bool config_builder_append(config_builder_t *builder, const char *fmt, ...) {
1194 if (builder->overflow) {
1195 return false;
1196 }
1197
1198 if (builder->size >= builder->capacity) {
1199 builder->overflow = true;
1200 return false;
1201 }
1202
1203 va_list args;
1204 va_start(args, fmt);
1205 int written = safe_vsnprintf(builder->buffer + builder->size, builder->capacity - builder->size, fmt, args);
1206 va_end(args);
1207
1208 if (written < 0 || builder->size + written >= builder->capacity) {
1209 builder->overflow = true;
1210 return false;
1211 }
1212
1213 builder->size += written;
1214 return true;
1215}
1216
1217static bool config_key_should_be_commented(const char *toml_key) {
1218 if (!toml_key) {
1219 return false;
1220 }
1221
1222 return strcmp(toml_key, "logging.log_file") == 0 || strcmp(toml_key, "security.password") == 0 ||
1223 strcmp(toml_key, "security.key") == 0 || strcmp(toml_key, "security.server_key") == 0 ||
1224 strcmp(toml_key, "security.client_keys") == 0 || strcmp(toml_key, "media.file") == 0 ||
1225 strcmp(toml_key, "media.url") == 0 || strcmp(toml_key, "media.yt_dlp_options") == 0 ||
1226 strcmp(toml_key, "network.turn_secret") == 0;
1227}
1228
1234static asciichat_mode_t extract_mode_from_bitmask(option_mode_bitmask_t mode_bitmask) {
1235 // Check each mode bit
1236 if (mode_bitmask == OPTION_MODE_SERVER)
1237 return MODE_SERVER;
1238 if (mode_bitmask == OPTION_MODE_CLIENT)
1239 return MODE_CLIENT;
1240 if (mode_bitmask == OPTION_MODE_MIRROR)
1241 return MODE_MIRROR;
1242 if (mode_bitmask == OPTION_MODE_DISCOVERY_SVC)
1243 return MODE_DISCOVERY_SERVICE;
1244 if (mode_bitmask == OPTION_MODE_DISCOVERY)
1245 return MODE_DISCOVERY;
1246 // Bitmask has zero or multiple bits set
1247 return MODE_INVALID;
1248}
1249
1250asciichat_error_t config_create_default(const char *config_path) {
1251 char *config_path_expanded = NULL;
1252
1253 defer(SAFE_FREE(config_path_expanded));
1254
1255 // Create fresh options with all OPT_*_DEFAULT values
1256 options_t defaults = options_t_new();
1257
1258 // Allocate buffer for building config content (256KB should be plenty)
1259 const size_t BUFFER_CAPACITY = 256 * 1024;
1260 config_builder_t builder = {0};
1261 builder.buffer = SAFE_MALLOC(BUFFER_CAPACITY, char *);
1262 if (!builder.buffer) {
1263 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate config buffer");
1264 }
1265 defer(SAFE_FREE(builder.buffer));
1266 builder.capacity = BUFFER_CAPACITY;
1267
1268 // Build version comment in buffer
1269 if (!config_builder_append(&builder, "# ascii-chat configuration file\n")) {
1270 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1271 }
1272 if (!config_builder_append(&builder, "# Generated by ascii-chat v%d.%d.%d-%s\n", ASCII_CHAT_VERSION_MAJOR,
1273 ASCII_CHAT_VERSION_MINOR, ASCII_CHAT_VERSION_PATCH, ASCII_CHAT_GIT_VERSION)) {
1274 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1275 }
1276 if (!config_builder_append(&builder, "#\n")) {
1277 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1278 }
1279 if (!config_builder_append(&builder, "# All options below are commented out because some configuration options\n")) {
1280 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1281 }
1282 if (!config_builder_append(&builder, "# conflict with each other (e.g., --file vs --url, --loop vs --url).\n")) {
1283 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1284 }
1285 if (!config_builder_append(&builder, "# Uncomment only the options you need and avoid conflicting combinations.\n")) {
1286 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1287 }
1288 if (!config_builder_append(&builder, "#\n")) {
1289 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1290 }
1291 if (!config_builder_append(&builder,
1292 "# If you upgrade ascii-chat and this version comment changes, you may need to\n")) {
1293 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1294 }
1295 if (!config_builder_append(&builder, "# delete and regenerate this file with: ascii-chat --config-create\n")) {
1296 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1297 }
1298 if (!config_builder_append(&builder, "#\n\n")) {
1299 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1300 }
1301
1302 // Get all options from schema
1303 size_t metadata_count = 0;
1304 const config_option_metadata_t *metadata = config_schema_get_all(&metadata_count);
1305
1306 // Build list of unique categories in order of first appearance
1307 const char *categories[16] = {0}; // Max expected categories
1308 size_t category_count = 0;
1309
1310 for (size_t i = 0; i < metadata_count && category_count < 16; i++) {
1311 const char *category = metadata[i].category;
1312 if (!category) {
1313 continue;
1314 }
1315
1316 // Check if category already in list
1317 bool found = false;
1318 for (size_t j = 0; j < category_count; j++) {
1319 if (categories[j] && strcmp(categories[j], category) == 0) {
1320 found = true;
1321 break;
1322 }
1323 }
1324
1325 if (!found) {
1326 categories[category_count++] = category;
1327 }
1328 }
1329
1330 // Build each section dynamically from schema
1331 for (size_t cat_idx = 0; cat_idx < category_count; cat_idx++) {
1332 const char *category = categories[cat_idx];
1333 if (!category) {
1334 continue;
1335 }
1336
1337 // Get all options for this category
1338 size_t cat_option_count = 0;
1339 const config_option_metadata_t **cat_options = config_schema_get_by_category(category, &cat_option_count);
1340
1341 if (!cat_options || cat_option_count == 0) {
1342 continue;
1343 }
1344
1345 // Add section header
1346 if (!config_builder_append(&builder, "[%s]\n", category)) {
1347 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1348 }
1349
1350 // Track which options we've written (to avoid duplicates)
1351 bool written_flags[64] = {0}; // Max options per category
1352
1353 // Add each option in this category
1354 for (size_t opt_idx = 0; opt_idx < cat_option_count && opt_idx < 64; opt_idx++) {
1355 const config_option_metadata_t *meta = cat_options[opt_idx];
1356 if (!meta || !meta->toml_key) {
1357 continue;
1358 }
1359
1360 // Skip if already written (duplicate)
1361 if (written_flags[opt_idx]) {
1362 continue;
1363 }
1364
1365 // Skip if this is a duplicate of another option (check by TOML key, not field_offset)
1366 // Note: Multiple options can map to the same field (e.g., server_log_file, client_log_file)
1367 bool is_duplicate = false;
1368 for (size_t j = 0; j < opt_idx; j++) {
1369 if (cat_options[j] && cat_options[j]->toml_key && meta->toml_key &&
1370 strcmp(cat_options[j]->toml_key, meta->toml_key) == 0) {
1371 is_duplicate = true;
1372 break;
1373 }
1374 }
1375 if (is_duplicate) {
1376 continue;
1377 }
1378
1379 // Get field pointer from default options (or mode-specific default if available)
1380 const char *field_ptr = ((const char *)&defaults) + meta->field_offset;
1381
1382 // Buffer to hold mode-specific default value if needed
1383 char mode_default_buffer[OPTIONS_BUFF_SIZE] = {0};
1384 int mode_default_int = 0;
1385
1386 // If this option has a mode_default_getter, use it to get the correct default
1387 if (meta->mode_default_getter) {
1388 asciichat_mode_t mode = extract_mode_from_bitmask(meta->mode_bitmask);
1389 if (mode != MODE_INVALID) {
1390 const void *default_value = meta->mode_default_getter(mode);
1391 if (default_value) {
1392 // Copy the default value to our buffer based on type
1393 if (meta->type == OPTION_TYPE_STRING || meta->type == OPTION_TYPE_CALLBACK) {
1394 const char *str_value = (const char *)default_value;
1395 SAFE_STRNCPY(mode_default_buffer, str_value, sizeof(mode_default_buffer));
1396 field_ptr = mode_default_buffer;
1397 } else if (meta->type == OPTION_TYPE_INT) {
1398 mode_default_int = *(const int *)default_value;
1399 field_ptr = (const char *)&mode_default_int;
1400 }
1401 }
1402 }
1403 }
1404
1405 // Add description comment if available
1406 if (meta->description && strlen(meta->description) > 0) {
1407 if (!config_builder_append(&builder, "# %s\n", meta->description)) {
1408 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1409 }
1410 }
1411
1412 // Format and add the option value using handler (commented out to avoid conflicts)
1413 if (g_type_handlers[meta->type].format_output) {
1414 char formatted_value[BUFFER_SIZE_MEDIUM] = {0};
1415 g_type_handlers[meta->type].format_output(field_ptr, meta->field_size, meta, formatted_value,
1416 sizeof(formatted_value));
1417 const char *output_key = meta->toml_key;
1418 size_t category_len = strlen(category);
1419 if (strncmp(meta->toml_key, category, category_len) == 0 && meta->toml_key[category_len] == '.') {
1420 output_key = meta->toml_key + category_len + 1; // Strip "<category>."
1421 }
1422
1423 if (config_key_should_be_commented(meta->toml_key)) {
1424 if (!config_builder_append(&builder, "# %s = %s\n", output_key, formatted_value)) {
1425 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1426 }
1427 } else {
1428 if (!config_builder_append(&builder, "%s = %s\n", output_key, formatted_value)) {
1429 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1430 }
1431 }
1432 }
1433
1434 // Add blank line after each option
1435 if (!config_builder_append(&builder, "\n")) {
1436 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1437 }
1438
1439 written_flags[opt_idx] = true;
1440 }
1441
1442 // Add blank line between sections (but not after the last section)
1443 if (cat_idx < category_count - 1) {
1444 if (!config_builder_append(&builder, "\n")) {
1445 return SET_ERRNO(ERROR_CONFIG, "Config too large to fit in buffer");
1446 }
1447 }
1448 }
1449
1450 // Now write the buffer to either stdout or a file
1451 if (config_path && strlen(config_path) > 0) {
1452 // User provided a filepath - write to that file with overwrite prompt
1453
1454 // Expand and validate the path
1455 config_path_expanded = expand_path(config_path);
1456 if (!config_path_expanded) {
1457 config_path_expanded = platform_strdup(config_path);
1458 }
1459
1460 if (!config_path_expanded) {
1461 return SET_ERRNO(ERROR_CONFIG, "Failed to resolve config file path");
1462 }
1463
1464 char *validated_config_path = NULL;
1465 asciichat_error_t validate_result =
1466 path_validate_user_path(config_path_expanded, PATH_ROLE_CONFIG_FILE, &validated_config_path);
1467 if (validate_result != ASCIICHAT_OK) {
1468 SAFE_FREE(validated_config_path);
1469 SAFE_FREE(config_path_expanded);
1470 return validate_result;
1471 }
1472 // Free the old path before reassigning (defer will free the new one)
1473 if (config_path_expanded != validated_config_path) {
1474 SAFE_FREE(config_path_expanded);
1475 }
1476 config_path_expanded = validated_config_path;
1477
1478 // Check if file already exists
1479 struct stat st;
1480 if (stat(config_path_expanded, &st) == 0) {
1481 // File exists - ask user if they want to overwrite
1482 log_plain("Config file already exists: %s", config_path_expanded);
1483
1484 bool overwrite = platform_prompt_yes_no("Overwrite", false); // Default to No
1485 if (!overwrite) {
1486 log_plain("Config file creation cancelled.");
1487 return SET_ERRNO(ERROR_CONFIG, "User cancelled overwrite");
1488 }
1489
1490 log_plain("Overwriting existing config file...");
1491 }
1492
1493 // Create directory if needed
1494 char *dir_path = platform_strdup(config_path_expanded);
1495 if (!dir_path) {
1496 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate memory for directory path");
1497 }
1498 defer(SAFE_FREE(dir_path));
1499
1500 // Find the last path separator
1501 char *last_sep = strrchr(dir_path, PATH_DELIM);
1502
1503 if (last_sep) {
1504 *last_sep = '\0';
1505 // Create directory recursively
1506 asciichat_error_t mkdir_result = platform_mkdir_recursive(dir_path, DIR_PERM_PRIVATE);
1507 if (mkdir_result != ASCIICHAT_OK) {
1508 return mkdir_result;
1509 }
1510 }
1511
1512 // Open file for writing
1513 FILE *output_file = platform_fopen(config_path_expanded, "w");
1514 if (!output_file) {
1515 return SET_ERRNO_SYS(ERROR_CONFIG, "Failed to open config file for writing: %s", config_path_expanded);
1516 }
1517 defer(SAFE_FCLOSE(output_file));
1518
1519 // Write buffer to file
1520 size_t written = fwrite(builder.buffer, 1, builder.size, output_file);
1521 if (written != builder.size) {
1522 return SET_ERRNO_SYS(ERROR_CONFIG, "Failed to write config to file: %s", config_path_expanded);
1523 }
1524 } else {
1525 // No filepath provided - write buffer to stdout with automatic retry on transient errors
1526 (void)platform_write_all(STDOUT_FILENO, builder.buffer, builder.size);
1527 // Flush C stdio buffer and terminal to ensure piped output is written immediately
1528 (void)fflush(stdout);
1529 (void)terminal_flush(STDOUT_FILENO);
1530 }
1531
1532 return ASCIICHAT_OK;
1533}
1534
1535asciichat_error_t config_load_system_and_user(asciichat_mode_t detected_mode, bool strict, options_t *opts) {
1536 // Use platform abstraction to find all config.toml files across standard locations
1537 config_file_list_t config_files = {0};
1538 asciichat_error_t search_result = platform_find_config_file("config.toml", &config_files);
1539 defer(config_file_list_destroy(&config_files));
1540
1541 if (search_result != ASCIICHAT_OK) {
1542 CONFIG_DEBUG("Failed to search for config files: %d", search_result);
1543 config_file_list_destroy(&config_files);
1544 return search_result;
1545 }
1546
1547 // Cascade load: Load all found configs in reverse order (lowest priority first)
1548 // This allows higher-priority configs to override lower-priority values.
1549 // Example: System configs load first, then user configs override them.
1550
1551 asciichat_error_t result = ASCIICHAT_OK;
1552 for (size_t i = config_files.count; i > 0; i--) {
1553 const config_file_result_t *file = &config_files.files[i - 1];
1554
1555 // Determine strictness based on whether this is a system or user config
1556 // System configs are non-strict (values can be missing, errors are non-fatal)
1557 // User config is strict or non-strict based on parameter
1558 bool is_user_config = !file->is_system_config;
1559 bool file_strict = is_user_config ? strict : false;
1560
1561 CONFIG_DEBUG("Loading config from %s (system=%s, strict=%s)", file->path, file->is_system_config ? "yes" : "no",
1562 file_strict ? "true" : "false");
1563
1564 asciichat_error_t load_result = config_load_and_apply(detected_mode, file->path, file_strict, opts);
1565
1566 if (load_result != ASCIICHAT_OK) {
1567 if (file_strict) {
1568 // Strict mode: errors are fatal
1569 CONFIG_DEBUG("Strict config loading failed for %s", file->path);
1570 result = load_result;
1571 } else {
1572 // Non-strict mode: errors are non-fatal, just log and continue
1573 CONFIG_DEBUG("Non-strict config loading warning for %s: %d (continuing)", file->path, load_result);
1574 CLEAR_ERRNO(); // Clear error context for next file
1575 }
1576 }
1577 }
1578
1579 config_file_list_destroy(&config_files);
1580 return result;
1581}
size_t platform_write_all(int fd, const void *buf, size_t count)
Write all data to file descriptor with automatic retry on transient errors.
Definition abstraction.c:39
color_filter_t color_filter_from_cli_name(const char *cli_name)
const color_filter_def_t * color_filter_get_metadata(color_filter_t filter)
asciichat_error_t config_load_system_and_user(asciichat_mode_t detected_mode, bool strict, options_t *opts)
Definition config.c:1535
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:1044
asciichat_error_t config_create_default(const char *config_path)
Definition config.c:1250
#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
options_t options_t_new(void)
bool log_get_terminal_output(void)
action_args_t args
bool path_looks_like_path(const char *value)
Definition path.c:766
asciichat_error_t path_validate_user_path(const char *input, path_role_t role, char **normalized_out)
Definition path.c:974
char * expand_path(const char *path)
Definition path.c:471
char * get_config_dir(void)
Definition path.c:493
char * platform_strdup(const char *s)
asciichat_error_t terminal_flush(int fd)
asciichat_error_t options_state_set(const options_t *opts)
Definition rcu.c:284
const config_option_metadata_t ** config_schema_get_by_category(const char *category, size_t *count)
Definition schema.c:393
const config_option_metadata_t * config_schema_get_all(size_t *count)
Definition schema.c:426
Helper structure for building config content in a buffer.
Definition config.c:1180
size_t capacity
Definition config.c:1183
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
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
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:507
Union holding all possible parsed option values.
Definition config.c:121
char str_value[OPTIONS_BUFF_SIZE]
Definition config.c:122
bool utf8_is_valid(const char *str)
Definition utf8.c:158
bool platform_prompt_yes_no(const char *question, bool default_yes)
Definition util.c:81
int validate_opt_render_mode(const char *value_str, char *error_msg, size_t error_msg_size)
Definition validation.c:173
int validate_opt_color_mode(const char *value_str, char *error_msg, size_t error_msg_size)
Definition validation.c:139
int validate_opt_palette(const char *value_str, char *error_msg, size_t error_msg_size)
Definition validation.c:201
FILE * platform_fopen(const char *filename, const char *mode)
asciichat_error_t platform_find_config_file(const char *filename, config_file_list_t *list_out)
asciichat_error_t platform_mkdir_recursive(const char *path, int mode)
void config_file_list_destroy(config_file_list_t *list)