7#include <ascii-chat/options/parsers.h>
12#include <ascii-chat/common.h>
13#include <ascii-chat/platform/util.h>
14#include <ascii-chat/options/builder.h>
15#include <ascii-chat/options/options.h>
16#include <ascii-chat/options/strings.h>
17#include <ascii-chat/discovery/strings.h>
18#include <ascii-chat/util/parsing.h>
19#include <ascii-chat/util/path.h>
20#include <ascii-chat/util/pcre2.h>
21#include <ascii-chat/util/time.h>
22#include <ascii-chat/video/color_filter.h>
26static void to_lower(
const char *src,
char *dst,
size_t max_len) {
28 while (src[i] && i < max_len - 1) {
29 dst[i] = (char)tolower((
unsigned char)src[i]);
46static const char *SETTING_PATTERN =
"^(auto|a|0|true|yes|1|on|enabled|enable|false|no|-1|off|disabled|disable)$";
54static pcre2_code *setting_regex_get(
void) {
55 if (g_setting_regex == NULL) {
56 g_setting_regex = asciichat_pcre2_singleton_compile(SETTING_PATTERN, 0);
81static bool parse_setting_generic(
const char *arg,
void *dest,
const setting_map_entry_t *lookup_table,
83 if (!dest || !lookup_table) {
85 *error_msg =
platform_strdup(
"Internal error: NULL destination or lookup table");
90 int *result = (
int *)dest;
93 if (!arg || arg[0] ==
'\0') {
100 to_lower(arg, lower,
sizeof(lower));
103 pcre2_code *regex = setting_regex_get();
106 *error_msg =
platform_strdup(
"Internal error: PCRE2 regex not available");
111 pcre2_match_data *match_data = pcre2_match_data_create_from_pattern(regex, NULL);
114 *error_msg =
platform_strdup(
"Internal error: Failed to allocate match data");
119 int rc = pcre2_jit_match(regex, (PCRE2_SPTR8)lower, strlen(lower), 0, 0, match_data, NULL);
120 pcre2_match_data_free(match_data);
131 for (
int i = 0; lookup_table[i].
match != NULL; i++) {
132 if (strcmp(lower, lookup_table[i].match) == 0) {
140 *error_msg =
platform_strdup(
"Internal error: Regex matched but lookup failed");
151 {
"auto", COLOR_SETTING_AUTO}, {
"a", COLOR_SETTING_AUTO},
152 {
"0", COLOR_SETTING_AUTO}, {
"true", COLOR_SETTING_TRUE},
153 {
"yes", COLOR_SETTING_TRUE}, {
"1", COLOR_SETTING_TRUE},
154 {
"on", COLOR_SETTING_TRUE}, {
"enabled", COLOR_SETTING_TRUE},
155 {
"enable", COLOR_SETTING_TRUE}, {
"false", COLOR_SETTING_FALSE},
156 {
"no", COLOR_SETTING_FALSE}, {
"-1", COLOR_SETTING_FALSE},
157 {
"off", COLOR_SETTING_FALSE}, {
"disabled", COLOR_SETTING_FALSE},
158 {
"disable", COLOR_SETTING_FALSE}, {NULL, 0}
171 if (!arg || arg[0] ==
'\0') {
172 int *color_setting = (
int *)dest;
173 *color_setting = COLOR_SETTING_TRUE;
177 return parse_setting_generic(arg, dest, g_color_setting_map, error_msg);
182 {
"auto", UTF8_SETTING_AUTO}, {
"a", UTF8_SETTING_AUTO},
183 {
"0", UTF8_SETTING_AUTO}, {
"true", UTF8_SETTING_TRUE},
184 {
"yes", UTF8_SETTING_TRUE}, {
"1", UTF8_SETTING_TRUE},
185 {
"on", UTF8_SETTING_TRUE}, {
"enabled", UTF8_SETTING_TRUE},
186 {
"enable", UTF8_SETTING_TRUE}, {
"false", UTF8_SETTING_FALSE},
187 {
"no", UTF8_SETTING_FALSE}, {
"-1", UTF8_SETTING_FALSE},
188 {
"off", UTF8_SETTING_FALSE}, {
"disabled", UTF8_SETTING_FALSE},
189 {
"disable", UTF8_SETTING_FALSE}, {NULL, 0}
202 if (!arg || arg[0] ==
'\0') {
203 int *utf8_setting = (
int *)dest;
204 *utf8_setting = UTF8_SETTING_TRUE;
208 return parse_setting_generic(arg, dest, g_utf8_setting_map, error_msg);
214 *error_msg =
platform_strdup(
"Internal error: NULL argument or destination");
219 terminal_color_mode_t *color_mode = (terminal_color_mode_t *)dest;
221 to_lower(arg, lower,
sizeof(lower));
224 if (strcmp(lower,
"auto") == 0 || strcmp(lower,
"a") == 0) {
225 *color_mode = TERM_COLOR_AUTO;
230 if (strcmp(lower,
"none") == 0 || strcmp(lower,
"mono") == 0 || strcmp(lower,
"monochrome") == 0 ||
231 strcmp(lower,
"0") == 0) {
232 *color_mode = TERM_COLOR_NONE;
237 if (strcmp(lower,
"16") == 0 || strcmp(lower,
"16color") == 0 || strcmp(lower,
"ansi") == 0 ||
238 strcmp(lower,
"1") == 0) {
239 *color_mode = TERM_COLOR_16;
244 if (strcmp(lower,
"256") == 0 || strcmp(lower,
"256color") == 0 || strcmp(lower,
"2") == 0) {
245 *color_mode = TERM_COLOR_256;
250 if (strcmp(lower,
"truecolor") == 0 || strcmp(lower,
"true") == 0 || strcmp(lower,
"tc") == 0 ||
251 strcmp(lower,
"rgb") == 0 || strcmp(lower,
"24bit") == 0 || strcmp(lower,
"3") == 0) {
252 *color_mode = TERM_COLOR_TRUECOLOR;
261 safe_snprintf(msg,
sizeof(msg),
"Invalid color mode '%s'. Did you mean '%s'?", arg, suggestion);
263 safe_snprintf(msg,
sizeof(msg),
"Invalid color mode '%s'. Valid values: auto, none, 16, 256, truecolor", arg);
273 *error_msg =
platform_strdup(
"Internal error: NULL argument or destination");
278 color_filter_t *color_filter = (color_filter_t *)dest;
280 to_lower(arg, lower,
sizeof(lower));
284 if (*color_filter != COLOR_FILTER_NONE || strcmp(lower,
"none") == 0) {
292 "Invalid color filter '%s'. Valid values: none, black, white, green, magenta, fuchsia, "
293 "orange, teal, cyan, pink, red, yellow",
303 *error_msg =
platform_strdup(
"Internal error: NULL argument or destination");
308 render_mode_t *render_mode = (render_mode_t *)dest;
310 to_lower(arg, lower,
sizeof(lower));
313 if (strcmp(lower,
"foreground") == 0 || strcmp(lower,
"fg") == 0 || strcmp(lower,
"0") == 0) {
314 *render_mode = RENDER_MODE_FOREGROUND;
319 if (strcmp(lower,
"background") == 0 || strcmp(lower,
"bg") == 0 || strcmp(lower,
"1") == 0) {
320 *render_mode = RENDER_MODE_BACKGROUND;
325 if (strcmp(lower,
"half-block") == 0 || strcmp(lower,
"hb") == 0 || strcmp(lower,
"2") == 0) {
326 *render_mode = RENDER_MODE_HALF_BLOCK;
335 safe_snprintf(msg,
sizeof(msg),
"Invalid render mode '%s'. Did you mean '%s'?", arg, suggestion);
337 safe_snprintf(msg,
sizeof(msg),
"Invalid render mode '%s'. Valid values: foreground, background, half-block",
348 *error_msg =
platform_strdup(
"Internal error: NULL argument or destination");
353 palette_type_t *palette_type = (palette_type_t *)dest;
355 to_lower(arg, lower,
sizeof(lower));
358 if (strcmp(lower,
"standard") == 0 || strcmp(lower,
"std") == 0 || strcmp(lower,
"0") == 0) {
359 *palette_type = PALETTE_STANDARD;
364 if (strcmp(lower,
"blocks") == 0 || strcmp(lower,
"block") == 0 || strcmp(lower,
"1") == 0) {
365 *palette_type = PALETTE_BLOCKS;
370 if (strcmp(lower,
"digital") == 0 || strcmp(lower,
"dig") == 0 || strcmp(lower,
"2") == 0) {
371 *palette_type = PALETTE_DIGITAL;
376 if (strcmp(lower,
"minimal") == 0 || strcmp(lower,
"min") == 0 || strcmp(lower,
"3") == 0) {
377 *palette_type = PALETTE_MINIMAL;
382 if (strcmp(lower,
"cool") == 0 || strcmp(lower,
"4") == 0) {
383 *palette_type = PALETTE_COOL;
388 if (strcmp(lower,
"custom") == 0 || strcmp(lower,
"5") == 0) {
389 *palette_type = PALETTE_CUSTOM;
398 safe_snprintf(msg,
sizeof(msg),
"Invalid palette type '%s'. Did you mean '%s'?", arg, suggestion);
401 "Invalid palette type '%s'. Valid values: standard, blocks, digital, minimal, cool, custom", arg);
416 log_level_t *log_level = (log_level_t *)dest;
419 if (!arg || arg[0] ==
'\0') {
420 *log_level = DEFAULT_LOG_LEVEL;
425 to_lower(arg, lower,
sizeof(lower));
428 if (strcmp(lower,
"dev") == 0 || strcmp(lower,
"development") == 0 || strcmp(lower,
"0") == 0) {
429 *log_level = LOG_DEV;
434 if (strcmp(lower,
"debug") == 0 || strcmp(lower,
"dbg") == 0 || strcmp(lower,
"1") == 0) {
435 *log_level = LOG_DEBUG;
440 if (strcmp(lower,
"info") == 0 || strcmp(lower,
"information") == 0 || strcmp(lower,
"2") == 0) {
441 *log_level = LOG_INFO;
446 if (strcmp(lower,
"warn") == 0 || strcmp(lower,
"warning") == 0 || strcmp(lower,
"3") == 0) {
447 *log_level = LOG_WARN;
452 if (strcmp(lower,
"error") == 0 || strcmp(lower,
"err") == 0 || strcmp(lower,
"4") == 0) {
453 *log_level = LOG_ERROR;
458 if (strcmp(lower,
"fatal") == 0 || strcmp(lower,
"5") == 0) {
459 *log_level = LOG_FATAL;
466 safe_snprintf(msg,
sizeof(msg),
"Invalid log level '%s'. Valid values: dev, debug, info, warn, error, fatal", arg);
475 *error_msg =
platform_strdup(
"Internal error: NULL argument or destination");
480 int *port_value = (
int *)dest;
484 asciichat_error_t err =
parse_port(arg, &port_num);
485 if (err != ASCIICHAT_OK) {
488 safe_snprintf(msg,
sizeof(msg),
"Invalid port '%s'. Port must be a number between 1 and 65535.", arg);
494 *port_value = (int)port_num;
502#include <ascii-chat/util/ip.h>
513 if (!arg || !config) {
515 *error_msg =
platform_strdup(
"Internal error: NULL argument or config");
522 char *address = (
char *)config + offsetof(
struct options_state, address);
523 char *address6 = (
char *)config + offsetof(
struct options_state, address6);
528 char parsed_addr[OPTIONS_BUFF_SIZE];
529 const char *addr_to_check = arg;
531 addr_to_check = parsed_addr;
538 if (address[0] !=
'\0' && !
is_localhost_ipv4(address) && strcmp(address,
"localhost") != 0 &&
539 strcmp(address,
"0.0.0.0") != 0) {
543 "Cannot specify multiple IPv4 addresses.\n"
546 address, addr_to_check);
551 SAFE_SNPRINTF(address, OPTIONS_BUFF_SIZE,
"%s", addr_to_check);
560 "Cannot specify multiple IPv6 addresses.\n"
563 address6, addr_to_check);
568 SAFE_SNPRINTF(address6, OPTIONS_BUFF_SIZE,
"%s", addr_to_check);
574 "Invalid IP address '%s'.\n"
575 "Server bind addresses must be valid IPv4 or IPv6 addresses.\n"
577 " ascii-chat server 0.0.0.0\n"
578 " ascii-chat server ::1\n"
579 " ascii-chat server 0.0.0.0 ::1",
587 if (remaining && num_remaining > 0 && remaining[0]) {
588 const char *second_arg = remaining[0];
589 memset(parsed_addr, 0,
sizeof(parsed_addr));
590 addr_to_check = second_arg;
592 addr_to_check = parsed_addr;
597 if (address[0] !=
'\0' && !
is_localhost_ipv4(address) && strcmp(address,
"localhost") != 0 &&
598 strcmp(address,
"0.0.0.0") != 0) {
607 SAFE_SNPRINTF(address, OPTIONS_BUFF_SIZE,
"%s", addr_to_check);
620 SAFE_SNPRINTF(address6, OPTIONS_BUFF_SIZE,
"%s", addr_to_check);
634int parse_client_address(
const char *arg,
void *config,
char **remaining,
int num_remaining,
char **error_msg) {
638 if (!arg || !config) {
640 *error_msg =
platform_strdup(
"Internal error: NULL argument or config");
645 log_debug(
"parse_client_address: Processing argument: '%s'", arg);
648 char *address = (
char *)config + offsetof(
struct options_state, address);
649 int *port = (
int *)((
char *)config + offsetof(
struct options_state, port));
653 if (strncmp(arg,
"ws://", 5) == 0 || strncmp(arg,
"wss://", 6) == 0) {
654 log_debug(
"Detected WebSocket URL: %s", arg);
655 SAFE_SNPRINTF(address, OPTIONS_BUFF_SIZE,
"%s", arg);
657 if (strncmp(arg,
"wss://", 6) == 0) {
658 bool *no_encrypt = (
bool *)config + offsetof(
struct options_state, no_encrypt);
660 log_debug(
"Auto-detected wss:// - setting no_encrypt=true (TLS handles encryption)");
669 log_debug(
"parse_client_address: is_session_string('%s') = %s", arg, is_session ?
"true" :
"false");
673 char *session_string = (
char *)config + offsetof(
struct options_state, session_string);
674 SAFE_SNPRINTF(session_string, SESSION_STRING_BUFFER_SIZE,
"%s", arg);
675 log_debug(
"parse_client_address: Stored session string: %s", arg);
680 log_debug(
"parse_client_address: Parsing as server address (not a session string)");
683 const char *colon = strrchr(arg,
':');
689 const char *closing_bracket = strchr(arg,
']');
690 if (closing_bracket && closing_bracket < colon) {
692 size_t addr_len = (size_t)(closing_bracket - arg - 1);
693 if (addr_len >= OPTIONS_BUFF_SIZE) {
699 SAFE_SNPRINTF(address, OPTIONS_BUFF_SIZE,
"%.*s", (
int)addr_len, arg + 1);
702 const char *port_str = colon + 1;
704 long port_num = strtol(port_str, &endptr, 10);
705 if (*endptr !=
'\0' || port_num < 1 || port_num > 65535) {
708 safe_snprintf(msg,
sizeof(msg),
"Invalid port number '%s'. Must be 1-65535.", port_str);
713 *port = (int)port_num;
718 size_t colon_count = 0;
719 for (
const char *p = arg; *p; p++) {
724 if (colon_count == 1) {
726 size_t addr_len = (size_t)(colon - arg);
727 if (addr_len >= OPTIONS_BUFF_SIZE) {
733 SAFE_SNPRINTF(address, OPTIONS_BUFF_SIZE,
"%.*s", (
int)addr_len, arg);
736 const char *port_str = colon + 1;
738 long port_num = strtol(port_str, &endptr, 10);
739 if (*endptr !=
'\0' || port_num < 1 || port_num > 65535) {
742 safe_snprintf(msg,
sizeof(msg),
"Invalid port number '%s'. Must be 1-65535.", port_str);
747 *port = (int)port_num;
750 SAFE_SNPRINTF(address, OPTIONS_BUFF_SIZE,
"%s", arg);
755 SAFE_SNPRINTF(address, OPTIONS_BUFF_SIZE,
"%s", arg);
759 bool has_dot = strchr(address,
'.') != NULL;
760 bool has_colon = strchr(address,
':') != NULL;
761 bool starts_with_digit = address[0] >=
'0' && address[0] <=
'9';
769 "Invalid IPv6 address '%s'.\n"
770 "IPv6 addresses must be valid hex notation with colons.\n"
771 "Examples: ::1, 2001:db8::1, fe80::1\n"
772 "Or use hostnames like example.com",
778 }
else if (has_dot && starts_with_digit) {
784 "Invalid IPv4 address '%s'.\n"
785 "IPv4 addresses must have exactly 4 octets (0-255) separated by dots.\n"
786 "Examples: 127.0.0.1, 192.168.1.1\n"
787 "For hostnames, use letters: example.com, localhost",
800 log_debug(
"parse_client_address: Set address='%s', port=%d", address[0] ? address :
"(empty)", *port);
812 *error_msg =
platform_strdup(
"Internal error: NULL argument or destination");
821 char *palette_custom = (
char *)dest;
823 size_t len = strlen(arg);
826 *error_msg =
platform_strdup(
"Invalid palette-chars: value cannot be empty");
834 safe_snprintf(msg,
sizeof(msg),
"Invalid palette-chars: too long (%zu chars, max 255)", len);
841 SAFE_STRNCPY(palette_custom, arg, 256);
842 palette_custom[255] =
'\0';
846 options_t *opts = (options_t *)((
char *)dest - offsetof(options_t, palette_custom));
847 opts->palette_type = PALETTE_CUSTOM;
857 unsigned short int *verbose_level = (
unsigned short int *)dest;
859 if (!arg || arg[0] ==
'\0') {
867 long value = strtol(arg, &endptr, 10);
868 if (*endptr ==
'\0' && value >= 0 && value <= 100) {
869 *verbose_level = (
unsigned short int)value;
891 if (!arg || arg[0] ==
'\0') {
898 double *timestamp = (
double *)dest;
904 for (
const char *p = arg; *p; p++) {
909 if (colon_count == 0) {
911 *timestamp = strtod(arg, &endptr);
912 if (*endptr !=
'\0' || *timestamp < 0.0) {
914 *error_msg =
platform_strdup(
"Invalid timestamp: expected non-negative seconds");
919 }
else if (colon_count == 1) {
921 strtol_result = strtol(arg, &endptr, 10);
922 if (*endptr !=
':' || strtol_result < 0) {
924 *error_msg =
platform_strdup(
"Invalid timestamp: expected MM:SS or MM:SS.ms format");
928 long minutes = strtol_result;
929 double seconds = strtod(endptr + 1, &endptr);
930 if (*endptr !=
'\0' && *endptr !=
'.' && *endptr !=
'\0') {
932 *error_msg =
platform_strdup(
"Invalid timestamp: expected MM:SS or MM:SS.ms format");
936 *timestamp = minutes * 60.0 + seconds;
938 }
else if (colon_count == 2) {
940 strtol_result = strtol(arg, &endptr, 10);
941 if (*endptr !=
':' || strtol_result < 0) {
943 *error_msg =
platform_strdup(
"Invalid timestamp: expected HH:MM:SS or HH:MM:SS.ms format");
947 long hours = strtol_result;
949 strtol_result = strtol(endptr + 1, &endptr, 10);
950 if (*endptr !=
':' || strtol_result < 0 || strtol_result >= 60) {
952 *error_msg =
platform_strdup(
"Invalid timestamp: minutes must be 0-59");
956 long minutes = strtol_result;
958 double seconds = strtod(endptr + 1, &endptr);
959 if (*endptr !=
'\0') {
961 *error_msg =
platform_strdup(
"Invalid timestamp: expected HH:MM:SS or HH:MM:SS.ms format");
965 *timestamp = hours * (double)SEC_PER_HOUR + minutes * (
double)SEC_PER_MIN + seconds;
969 *error_msg =
platform_strdup(
"Invalid timestamp format: too many colons");
978 *error_msg =
platform_strdup(
"Internal error: NULL argument or destination");
983 float *volume = (
float *)dest;
985 float val = strtof(arg, &endptr);
987 if (*endptr !=
'\0' || arg == endptr) {
989 *error_msg =
platform_strdup(
"Invalid volume value. Must be a number between 0.0 and 1.0");
994 if (val < 0.0f || val > 1.0f) {
997 SAFE_SNPRINTF(buf,
sizeof(buf),
"Volume must be between 0.0 and 1.0 (got %.2f)", val);
1008 if (!arg || !dest) {
1010 *error_msg =
platform_strdup(
"Internal error: NULL argument or destination");
1016 char *normalized = NULL;
1019 if (result != ASCIICHAT_OK) {
1021 asciichat_error_context_t err_ctx;
1022 if (HAS_ERRNO(&err_ctx)) {
1032 char *log_file_buf = (
char *)dest;
1033 const size_t max_size = 256;
1034 SAFE_STRNCPY(log_file_buf, normalized, max_size - 1);
1035 log_file_buf[max_size - 1] =
'\0';
1037 SAFE_FREE(normalized);
1042 if (!arg || !dest) {
1044 *error_msg =
platform_strdup(
"Internal error: NULL argument or destination");
1049 audio_source_t *audio_source = (audio_source_t *)dest;
1051 to_lower(arg, lower,
sizeof(lower));
1054 if (strcmp(lower,
"auto") == 0) {
1055 *audio_source = AUDIO_SOURCE_AUTO;
1060 if (strcmp(lower,
"mic") == 0) {
1061 *audio_source = AUDIO_SOURCE_MIC;
1066 if (strcmp(lower,
"media") == 0) {
1067 *audio_source = AUDIO_SOURCE_MEDIA;
1072 if (strcmp(lower,
"both") == 0) {
1073 *audio_source = AUDIO_SOURCE_BOTH;
1078 *error_msg =
platform_strdup(
"Audio source must be 'auto', 'mic', 'media', or 'both'");
color_filter_t color_filter_from_cli_name(const char *cli_name)
bool is_session_string(const char *str)
int is_localhost_ipv6(const char *ip)
int is_valid_ipv4(const char *ip)
int is_valid_ipv6(const char *ip)
int is_localhost_ipv4(const char *ip)
int parse_ipv6_address(const char *input, char *output, size_t output_size)
const char * asciichat_suggest_enum_value(const char *option_name, const char *input)
bool parse_render_mode(const char *arg, void *dest, char **error_msg)
int parse_server_bind_address(const char *arg, void *config, char **remaining, int num_remaining, char **error_msg)
Parse server bind address positional argument.
bool parse_audio_source(const char *arg, void *dest, char **error_msg)
bool parse_port_option(const char *arg, void *dest, char **error_msg)
bool parse_timestamp(const char *arg, void *dest, char **error_msg)
Custom parser for –seek flag.
bool parse_palette_type(const char *arg, void *dest, char **error_msg)
bool parse_verbose_flag(const char *arg, void *dest, char **error_msg)
bool parse_color_filter(const char *arg, void *dest, char **error_msg)
bool parse_color_setting(const char *arg, void *dest, char **error_msg)
bool parse_log_file(const char *arg, void *dest, char **error_msg)
bool parse_volume(const char *arg, void *dest, char **error_msg)
bool parse_log_level(const char *arg, void *dest, char **error_msg)
int parse_client_address(const char *arg, void *config, char **remaining, int num_remaining, char **error_msg)
Parse client address positional argument.
bool parse_color_mode(const char *arg, void *dest, char **error_msg)
bool parse_utf8_setting(const char *arg, void *dest, char **error_msg)
bool parse_palette_chars(const char *arg, void *dest, char **error_msg)
asciichat_error_t parse_port(const char *str, uint16_t *out_port)
asciichat_error_t path_validate_user_path(const char *input, path_role_t role, char **normalized_out)
pcre2_code * asciichat_pcre2_singleton_get_code(pcre2_singleton_t *singleton)
Get the compiled pcre2_code from a singleton handle.
Represents a thread-safe compiled PCRE2 regex singleton.
Lookup table for setting string-to-enum mapping.
int enum_value
Corresponding enum value.
const char * match
Setting string (lowercased)
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.