ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
parsers.c
Go to the documentation of this file.
1
7#include <ascii-chat/options/parsers.h>
8#include <string.h>
9#include <stdlib.h>
10#include <ctype.h>
11#include <stddef.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> // For fuzzy matching suggestions
17#include <ascii-chat/discovery/strings.h> // For is_session_string() validation
18#include <ascii-chat/util/parsing.h> // For parse_port() validation
19#include <ascii-chat/util/path.h> // For path_validate_user_path()
20#include <ascii-chat/util/pcre2.h> // For centralized PCRE2 singleton
21#include <ascii-chat/util/time.h> // For SEC_PER_HOUR, SEC_PER_MIN
22#include <ascii-chat/video/color_filter.h> // For color_filter_from_cli_name()
23#include <pcre2.h>
24
25// Helper function to convert string to lowercase in-place (non-destructive)
26static void to_lower(const char *src, char *dst, size_t max_len) {
27 size_t i = 0;
28 while (src[i] && i < max_len - 1) {
29 dst[i] = (char)tolower((unsigned char)src[i]);
30 i++;
31 }
32 dst[i] = '\0';
33}
34
35// ═══════════════════════════════════════════════════════════════════════════
36// PCRE2 REGEX-BASED SETTING PARSER
37// ═══════════════════════════════════════════════════════════════════════════
38
46static const char *SETTING_PATTERN = "^(auto|a|0|true|yes|1|on|enabled|enable|false|no|-1|off|disabled|disable)$";
47
48static pcre2_singleton_t *g_setting_regex = NULL;
49
54static pcre2_code *setting_regex_get(void) {
55 if (g_setting_regex == NULL) {
56 g_setting_regex = asciichat_pcre2_singleton_compile(SETTING_PATTERN, 0);
57 }
58 return asciichat_pcre2_singleton_get_code(g_setting_regex);
59}
60
64typedef struct {
65 const char *match;
68
81static bool parse_setting_generic(const char *arg, void *dest, const setting_map_entry_t *lookup_table,
82 char **error_msg) {
83 if (!dest || !lookup_table) {
84 if (error_msg) {
85 *error_msg = platform_strdup("Internal error: NULL destination or lookup table");
86 }
87 return false;
88 }
89
90 int *result = (int *)dest;
91
92 // Handle optional argument - default to first entry in table (usually "auto")
93 if (!arg || arg[0] == '\0') {
94 *result = lookup_table[0].enum_value;
95 return true;
96 }
97
98 // Convert to lowercase
99 char lower[32];
100 to_lower(arg, lower, sizeof(lower));
101
102 // Get compiled regex (lazy initialization)
103 pcre2_code *regex = setting_regex_get();
104 if (!regex) {
105 if (error_msg) {
106 *error_msg = platform_strdup("Internal error: PCRE2 regex not available");
107 }
108 return false;
109 }
110
111 pcre2_match_data *match_data = pcre2_match_data_create_from_pattern(regex, NULL);
112 if (!match_data) {
113 if (error_msg) {
114 *error_msg = platform_strdup("Internal error: Failed to allocate match data");
115 }
116 return false;
117 }
118
119 int rc = pcre2_jit_match(regex, (PCRE2_SPTR8)lower, strlen(lower), 0, 0, match_data, NULL);
120 pcre2_match_data_free(match_data);
121
122 if (rc < 0) {
123 // No match - invalid setting
124 if (error_msg) {
125 *error_msg = platform_strdup("Invalid setting value");
126 }
127 return false;
128 }
129
130 // Linear search through lookup table to get enum value
131 for (int i = 0; lookup_table[i].match != NULL; i++) {
132 if (strcmp(lower, lookup_table[i].match) == 0) {
133 *result = lookup_table[i].enum_value;
134 return true;
135 }
136 }
137
138 // Should not reach here if regex validated correctly
139 if (error_msg) {
140 *error_msg = platform_strdup("Internal error: Regex matched but lookup failed");
141 }
142 return false;
143}
144
145// NOTE: is_session_string() is now imported from lib/discovery/strings.h
146// and provides enhanced validation against actual wordlists via hashtable lookup.
147// See that module for the full implementation.
148
149// Lookup table for color setting strings
150static const setting_map_entry_t g_color_setting_map[] = {
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} // Sentinel
159};
160
161bool parse_color_setting(const char *arg, void *dest, char **error_msg) {
162 if (!dest) {
163 if (error_msg) {
164 *error_msg = platform_strdup("Internal error: NULL destination");
165 }
166 return false;
167 }
168
169 // Use generic parser with color setting lookup table
170 // Default to TRUE if no arg provided
171 if (!arg || arg[0] == '\0') {
172 int *color_setting = (int *)dest;
173 *color_setting = COLOR_SETTING_TRUE;
174 return true;
175 }
176
177 return parse_setting_generic(arg, dest, g_color_setting_map, error_msg);
178}
179
180// Lookup table for UTF-8 setting strings (identical to color setting for boolean patterns)
181static const setting_map_entry_t g_utf8_setting_map[] = {
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} // Sentinel
190};
191
192bool parse_utf8_setting(const char *arg, void *dest, char **error_msg) {
193 if (!dest) {
194 if (error_msg) {
195 *error_msg = platform_strdup("Internal error: NULL destination");
196 }
197 return false;
198 }
199
200 // Use generic parser with UTF-8 setting lookup table
201 // Default to TRUE if no arg provided
202 if (!arg || arg[0] == '\0') {
203 int *utf8_setting = (int *)dest;
204 *utf8_setting = UTF8_SETTING_TRUE;
205 return true;
206 }
207
208 return parse_setting_generic(arg, dest, g_utf8_setting_map, error_msg);
209}
210
211bool parse_color_mode(const char *arg, void *dest, char **error_msg) {
212 if (!arg || !dest) {
213 if (error_msg) {
214 *error_msg = platform_strdup("Internal error: NULL argument or destination");
215 }
216 return false;
217 }
218
219 terminal_color_mode_t *color_mode = (terminal_color_mode_t *)dest;
220 char lower[32];
221 to_lower(arg, lower, sizeof(lower));
222
223 // Auto-detect
224 if (strcmp(lower, "auto") == 0 || strcmp(lower, "a") == 0) {
225 *color_mode = TERM_COLOR_AUTO;
226 return true;
227 }
228
229 // Monochrome/None
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;
233 return true;
234 }
235
236 // 16-color
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;
240 return true;
241 }
242
243 // 256-color
244 if (strcmp(lower, "256") == 0 || strcmp(lower, "256color") == 0 || strcmp(lower, "2") == 0) {
245 *color_mode = TERM_COLOR_256;
246 return true;
247 }
248
249 // Truecolor
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;
253 return true;
254 }
255
256 // Invalid value - suggest closest match
257 if (error_msg) {
258 char msg[256];
259 const char *suggestion = asciichat_suggest_enum_value("color-mode", arg);
260 if (suggestion) {
261 safe_snprintf(msg, sizeof(msg), "Invalid color mode '%s'. Did you mean '%s'?", arg, suggestion);
262 } else {
263 safe_snprintf(msg, sizeof(msg), "Invalid color mode '%s'. Valid values: auto, none, 16, 256, truecolor", arg);
264 }
265 *error_msg = platform_strdup(msg);
266 }
267 return false;
268}
269
270bool parse_color_filter(const char *arg, void *dest, char **error_msg) {
271 if (!arg || !dest) {
272 if (error_msg) {
273 *error_msg = platform_strdup("Internal error: NULL argument or destination");
274 }
275 return false;
276 }
277
278 color_filter_t *color_filter = (color_filter_t *)dest;
279 char lower[32];
280 to_lower(arg, lower, sizeof(lower));
281
282 // Try to match against all known color filters
283 *color_filter = color_filter_from_cli_name(lower);
284 if (*color_filter != COLOR_FILTER_NONE || strcmp(lower, "none") == 0) {
285 return true;
286 }
287
288 // Invalid value
289 if (error_msg) {
290 char msg[256];
291 safe_snprintf(msg, sizeof(msg),
292 "Invalid color filter '%s'. Valid values: none, black, white, green, magenta, fuchsia, "
293 "orange, teal, cyan, pink, red, yellow",
294 arg);
295 *error_msg = platform_strdup(msg);
296 }
297 return false;
298}
299
300bool parse_render_mode(const char *arg, void *dest, char **error_msg) {
301 if (!arg || !dest) {
302 if (error_msg) {
303 *error_msg = platform_strdup("Internal error: NULL argument or destination");
304 }
305 return false;
306 }
307
308 render_mode_t *render_mode = (render_mode_t *)dest;
309 char lower[32];
310 to_lower(arg, lower, sizeof(lower));
311
312 // Foreground mode
313 if (strcmp(lower, "foreground") == 0 || strcmp(lower, "fg") == 0 || strcmp(lower, "0") == 0) {
314 *render_mode = RENDER_MODE_FOREGROUND;
315 return true;
316 }
317
318 // Background mode
319 if (strcmp(lower, "background") == 0 || strcmp(lower, "bg") == 0 || strcmp(lower, "1") == 0) {
320 *render_mode = RENDER_MODE_BACKGROUND;
321 return true;
322 }
323
324 // Half-block mode
325 if (strcmp(lower, "half-block") == 0 || strcmp(lower, "hb") == 0 || strcmp(lower, "2") == 0) {
326 *render_mode = RENDER_MODE_HALF_BLOCK;
327 return true;
328 }
329
330 // Invalid value - suggest closest match
331 if (error_msg) {
332 char msg[256];
333 const char *suggestion = asciichat_suggest_enum_value("render-mode", arg);
334 if (suggestion) {
335 safe_snprintf(msg, sizeof(msg), "Invalid render mode '%s'. Did you mean '%s'?", arg, suggestion);
336 } else {
337 safe_snprintf(msg, sizeof(msg), "Invalid render mode '%s'. Valid values: foreground, background, half-block",
338 arg);
339 }
340 *error_msg = platform_strdup(msg);
341 }
342 return false;
343}
344
345bool parse_palette_type(const char *arg, void *dest, char **error_msg) {
346 if (!arg || !dest) {
347 if (error_msg) {
348 *error_msg = platform_strdup("Internal error: NULL argument or destination");
349 }
350 return false;
351 }
352
353 palette_type_t *palette_type = (palette_type_t *)dest;
354 char lower[32];
355 to_lower(arg, lower, sizeof(lower));
356
357 // Standard palette
358 if (strcmp(lower, "standard") == 0 || strcmp(lower, "std") == 0 || strcmp(lower, "0") == 0) {
359 *palette_type = PALETTE_STANDARD;
360 return true;
361 }
362
363 // Blocks palette
364 if (strcmp(lower, "blocks") == 0 || strcmp(lower, "block") == 0 || strcmp(lower, "1") == 0) {
365 *palette_type = PALETTE_BLOCKS;
366 return true;
367 }
368
369 // Digital palette
370 if (strcmp(lower, "digital") == 0 || strcmp(lower, "dig") == 0 || strcmp(lower, "2") == 0) {
371 *palette_type = PALETTE_DIGITAL;
372 return true;
373 }
374
375 // Minimal palette
376 if (strcmp(lower, "minimal") == 0 || strcmp(lower, "min") == 0 || strcmp(lower, "3") == 0) {
377 *palette_type = PALETTE_MINIMAL;
378 return true;
379 }
380
381 // Cool palette
382 if (strcmp(lower, "cool") == 0 || strcmp(lower, "4") == 0) {
383 *palette_type = PALETTE_COOL;
384 return true;
385 }
386
387 // Custom palette
388 if (strcmp(lower, "custom") == 0 || strcmp(lower, "5") == 0) {
389 *palette_type = PALETTE_CUSTOM;
390 return true;
391 }
392
393 // Invalid value - suggest closest match
394 if (error_msg) {
395 char msg[256];
396 const char *suggestion = asciichat_suggest_enum_value("palette", arg);
397 if (suggestion) {
398 safe_snprintf(msg, sizeof(msg), "Invalid palette type '%s'. Did you mean '%s'?", arg, suggestion);
399 } else {
400 safe_snprintf(msg, sizeof(msg),
401 "Invalid palette type '%s'. Valid values: standard, blocks, digital, minimal, cool, custom", arg);
402 }
403 *error_msg = platform_strdup(msg);
404 }
405 return false;
406}
407
408bool parse_log_level(const char *arg, void *dest, char **error_msg) {
409 if (!dest) {
410 if (error_msg) {
411 *error_msg = platform_strdup("Internal error: NULL destination");
412 }
413 return false;
414 }
415
416 log_level_t *log_level = (log_level_t *)dest;
417
418 // If no argument provided, use the default log level (based on build type)
419 if (!arg || arg[0] == '\0') {
420 *log_level = DEFAULT_LOG_LEVEL;
421 return true;
422 }
423
424 char lower[32];
425 to_lower(arg, lower, sizeof(lower));
426
427 // Development level
428 if (strcmp(lower, "dev") == 0 || strcmp(lower, "development") == 0 || strcmp(lower, "0") == 0) {
429 *log_level = LOG_DEV;
430 return true;
431 }
432
433 // Debug level
434 if (strcmp(lower, "debug") == 0 || strcmp(lower, "dbg") == 0 || strcmp(lower, "1") == 0) {
435 *log_level = LOG_DEBUG;
436 return true;
437 }
438
439 // Info level
440 if (strcmp(lower, "info") == 0 || strcmp(lower, "information") == 0 || strcmp(lower, "2") == 0) {
441 *log_level = LOG_INFO;
442 return true;
443 }
444
445 // Warning level
446 if (strcmp(lower, "warn") == 0 || strcmp(lower, "warning") == 0 || strcmp(lower, "3") == 0) {
447 *log_level = LOG_WARN;
448 return true;
449 }
450
451 // Error level
452 if (strcmp(lower, "error") == 0 || strcmp(lower, "err") == 0 || strcmp(lower, "4") == 0) {
453 *log_level = LOG_ERROR;
454 return true;
455 }
456
457 // Fatal level
458 if (strcmp(lower, "fatal") == 0 || strcmp(lower, "5") == 0) {
459 *log_level = LOG_FATAL;
460 return true;
461 }
462
463 // Invalid value
464 if (error_msg) {
465 char msg[256];
466 safe_snprintf(msg, sizeof(msg), "Invalid log level '%s'. Valid values: dev, debug, info, warn, error, fatal", arg);
467 *error_msg = platform_strdup(msg);
468 }
469 return false;
470}
471
472bool parse_port_option(const char *arg, void *dest, char **error_msg) {
473 if (!arg || !dest) {
474 if (error_msg) {
475 *error_msg = platform_strdup("Internal error: NULL argument or destination");
476 }
477 return false;
478 }
479
480 int *port_value = (int *)dest;
481 uint16_t port_num;
482
483 // Use the existing parse_port function for validation
484 asciichat_error_t err = parse_port(arg, &port_num);
485 if (err != ASCIICHAT_OK) {
486 if (error_msg) {
487 char msg[256];
488 safe_snprintf(msg, sizeof(msg), "Invalid port '%s'. Port must be a number between 1 and 65535.", arg);
489 *error_msg = platform_strdup(msg);
490 }
491 return false;
492 }
493
494 *port_value = (int)port_num;
495 return true;
496}
497
498// ============================================================================
499// Positional Argument Parsers
500// ============================================================================
501
502#include <ascii-chat/util/ip.h>
503#include <string.h>
504
512int parse_server_bind_address(const char *arg, void *config, char **remaining, int num_remaining, char **error_msg) {
513 if (!arg || !config) {
514 if (error_msg) {
515 *error_msg = platform_strdup("Internal error: NULL argument or config");
516 }
517 return -1;
518 }
519
520 // Assume config struct has address and address6 fields (OPTIONS_BUFF_SIZE each)
521 // This is a simplified version that assumes standard options_state layout
522 char *address = (char *)config + offsetof(struct options_state, address);
523 char *address6 = (char *)config + offsetof(struct options_state, address6);
524
525 int consumed = 0;
526
527 // Parse first argument (IPv4 or IPv6)
528 char parsed_addr[OPTIONS_BUFF_SIZE];
529 const char *addr_to_check = arg;
530 if (parse_ipv6_address(arg, parsed_addr, sizeof(parsed_addr)) == 0) {
531 addr_to_check = parsed_addr;
532 }
533
534 // Check if it's IPv4 or IPv6
535 if (is_valid_ipv4(addr_to_check)) {
536 // Check if we already have a non-default IPv4 address
537 // Allow overwriting defaults (localhost, 0.0.0.0)
538 if (address[0] != '\0' && !is_localhost_ipv4(address) && strcmp(address, "localhost") != 0 &&
539 strcmp(address, "0.0.0.0") != 0) {
540 if (error_msg) {
541 char msg[256];
542 safe_snprintf(msg, sizeof(msg),
543 "Cannot specify multiple IPv4 addresses.\n"
544 "Already have: %s\n"
545 "Cannot add: %s",
546 address, addr_to_check);
547 *error_msg = platform_strdup(msg);
548 }
549 return -1;
550 }
551 SAFE_SNPRINTF(address, OPTIONS_BUFF_SIZE, "%s", addr_to_check);
552 consumed = 1;
553 } else if (is_valid_ipv6(addr_to_check)) {
554 // Check if we already have a non-default IPv6 address
555 // Allow overwriting default (::1)
556 if (address6[0] != '\0' && !is_localhost_ipv6(address6)) {
557 if (error_msg) {
558 char msg[256];
559 safe_snprintf(msg, sizeof(msg),
560 "Cannot specify multiple IPv6 addresses.\n"
561 "Already have: %s\n"
562 "Cannot add: %s",
563 address6, addr_to_check);
564 *error_msg = platform_strdup(msg);
565 }
566 return -1;
567 }
568 SAFE_SNPRINTF(address6, OPTIONS_BUFF_SIZE, "%s", addr_to_check);
569 consumed = 1;
570 } else {
571 if (error_msg) {
572 char msg[512];
573 safe_snprintf(msg, sizeof(msg),
574 "Invalid IP address '%s'.\n"
575 "Server bind addresses must be valid IPv4 or IPv6 addresses.\n"
576 "Examples:\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",
580 arg);
581 *error_msg = platform_strdup(msg);
582 }
583 return -1;
584 }
585
586 // Try to parse second address if available
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;
591 if (parse_ipv6_address(second_arg, parsed_addr, sizeof(parsed_addr)) == 0) {
592 addr_to_check = parsed_addr;
593 }
594
595 if (is_valid_ipv4(addr_to_check)) {
596 // Second is IPv4
597 if (address[0] != '\0' && !is_localhost_ipv4(address) && strcmp(address, "localhost") != 0 &&
598 strcmp(address, "0.0.0.0") != 0) {
599 // Already have an IPv4, can't add another
600 return consumed;
601 }
602 if (is_valid_ipv4(arg)) {
603 // First was also IPv4, can't have two IPv4s
604 return consumed;
605 }
606 // First was IPv6, second is IPv4 - accept both
607 SAFE_SNPRINTF(address, OPTIONS_BUFF_SIZE, "%s", addr_to_check);
608 consumed = 2;
609 } else if (is_valid_ipv6(addr_to_check)) {
610 // Second is IPv6
611 if (address6[0] != '\0' && !is_localhost_ipv6(address6)) {
612 // Already have an IPv6, can't add another
613 return consumed;
614 }
615 if (is_valid_ipv6(arg)) {
616 // First was also IPv6, can't have two IPv6s
617 return consumed;
618 }
619 // First was IPv4, second is IPv6 - accept both
620 SAFE_SNPRINTF(address6, OPTIONS_BUFF_SIZE, "%s", addr_to_check);
621 consumed = 2;
622 }
623 }
624
625 return consumed;
626}
627
634int parse_client_address(const char *arg, void *config, char **remaining, int num_remaining, char **error_msg) {
635 (void)remaining;
636 (void)num_remaining;
637
638 if (!arg || !config) {
639 if (error_msg) {
640 *error_msg = platform_strdup("Internal error: NULL argument or config");
641 }
642 return -1;
643 }
644
645 log_debug("parse_client_address: Processing argument: '%s'", arg);
646
647 // Access address and port fields from options_state struct
648 char *address = (char *)config + offsetof(struct options_state, address);
649 int *port = (int *)((char *)config + offsetof(struct options_state, port));
650
651 // Check for WebSocket URL (ws:// or wss://) FIRST before session string validation
652 // WebSocket URLs are passed through without validation or port extraction
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);
656 // For wss://, default to no ACIP-level encryption (TLS handles it)
657 if (strncmp(arg, "wss://", 6) == 0) {
658 bool *no_encrypt = (bool *)config + offsetof(struct options_state, no_encrypt);
659 *no_encrypt = true;
660 log_debug("Auto-detected wss:// - setting no_encrypt=true (TLS handles encryption)");
661 }
662 // Don't set port - WebSocket transport handles URL parsing internally
663 return 1; // Consumed 1 argument
664 }
665
666 // Check if this is a session string (format: adjective-noun-noun)
667 // Session strings have exactly 2 hyphens, only lowercase letters, length 5-47
668 bool is_session = is_session_string(arg);
669 log_debug("parse_client_address: is_session_string('%s') = %s", arg, is_session ? "true" : "false");
670
671 if (is_session) {
672 // This is a session string, not a server address
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);
676 return 1; // Consumed 1 arg
677 }
678
679 // Not a session string, parse as server address
680 log_debug("parse_client_address: Parsing as server address (not a session string)");
681
682 // Check for port in address (format: address:port or [ipv6]:port)
683 const char *colon = strrchr(arg, ':');
684
685 if (colon != NULL) {
686 // Check if this is IPv6 with port [::1]:port or plain hostname:port
687 if (arg[0] == '[') {
688 // IPv6 with brackets: [address]:port
689 const char *closing_bracket = strchr(arg, ']');
690 if (closing_bracket && closing_bracket < colon) {
691 // Extract address (remove brackets)
692 size_t addr_len = (size_t)(closing_bracket - arg - 1);
693 if (addr_len >= OPTIONS_BUFF_SIZE) {
694 if (error_msg) {
695 *error_msg = platform_strdup("IPv6 address too long");
696 }
697 return -1;
698 }
699 SAFE_SNPRINTF(address, OPTIONS_BUFF_SIZE, "%.*s", (int)addr_len, arg + 1);
700
701 // Extract and validate port
702 const char *port_str = colon + 1;
703 char *endptr;
704 long port_num = strtol(port_str, &endptr, 10);
705 if (*endptr != '\0' || port_num < 1 || port_num > 65535) {
706 if (error_msg) {
707 char msg[256];
708 safe_snprintf(msg, sizeof(msg), "Invalid port number '%s'. Must be 1-65535.", port_str);
709 *error_msg = platform_strdup(msg);
710 }
711 return -1;
712 }
713 *port = (int)port_num;
714 }
715 } else {
716 // Check if it's IPv6 without brackets (no port allowed)
717 // or hostname/IPv4:port
718 size_t colon_count = 0;
719 for (const char *p = arg; *p; p++) {
720 if (*p == ':')
721 colon_count++;
722 }
723
724 if (colon_count == 1) {
725 // Likely hostname:port or IPv4:port
726 size_t addr_len = (size_t)(colon - arg);
727 if (addr_len >= OPTIONS_BUFF_SIZE) {
728 if (error_msg) {
729 *error_msg = platform_strdup("Address too long");
730 }
731 return -1;
732 }
733 SAFE_SNPRINTF(address, OPTIONS_BUFF_SIZE, "%.*s", (int)addr_len, arg);
734
735 // Extract and validate port
736 const char *port_str = colon + 1;
737 char *endptr;
738 long port_num = strtol(port_str, &endptr, 10);
739 if (*endptr != '\0' || port_num < 1 || port_num > 65535) {
740 if (error_msg) {
741 char msg[256];
742 safe_snprintf(msg, sizeof(msg), "Invalid port number '%s'. Must be 1-65535.", port_str);
743 *error_msg = platform_strdup(msg);
744 }
745 return -1;
746 }
747 *port = (int)port_num;
748 } else {
749 // Multiple colons - likely bare IPv6 address
750 SAFE_SNPRINTF(address, OPTIONS_BUFF_SIZE, "%s", arg);
751 }
752 }
753 } else {
754 // No colon - just an address
755 SAFE_SNPRINTF(address, OPTIONS_BUFF_SIZE, "%s", arg);
756 }
757
758 // Validate addresses using comprehensive IPv4/IPv6 detection
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';
762
763 // Potential IPv6 address (has colons) - validate as IPv6
764 if (has_colon) {
765 if (!is_valid_ipv6(address)) {
766 if (error_msg) {
767 char msg[512];
768 safe_snprintf(msg, sizeof(msg),
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",
773 address);
774 *error_msg = platform_strdup(msg);
775 }
776 return -1;
777 }
778 } else if (has_dot && starts_with_digit) {
779 // Potential IPv4 address (has dots and starts with digit) - validate strictly
780 if (!is_valid_ipv4(address)) {
781 if (error_msg) {
782 char msg[512];
783 safe_snprintf(msg, sizeof(msg),
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",
788 address);
789 *error_msg = platform_strdup(msg);
790 }
791 return -1;
792 }
793 }
794 // Otherwise treat as valid hostname (no validation needed)
795
796 // Note: Port conflict checking would require additional state
797 // (checking if --port flag was used). For now, this is a simplified version.
798 // Full implementation would need to track whether port was set via flag.
799
800 log_debug("parse_client_address: Set address='%s', port=%d", address[0] ? address : "(empty)", *port);
801
802 return 1; // Consumed 1 arg
803}
804
805// ============================================================================
806// Palette Characters Parser
807// ============================================================================
808
809bool parse_palette_chars(const char *arg, void *dest, char **error_msg) {
810 if (!arg || !dest) {
811 if (error_msg) {
812 *error_msg = platform_strdup("Internal error: NULL argument or destination");
813 }
814 return false;
815 }
816
817 // The dest pointer points to the palette_custom field in options_t
818 // We need to get the full options_t struct to call parse_palette_chars_option
819 // Since we only have the field pointer, we need to handle this directly
820
821 char *palette_custom = (char *)dest;
822
823 size_t len = strlen(arg);
824 if (len == 0) {
825 if (error_msg) {
826 *error_msg = platform_strdup("Invalid palette-chars: value cannot be empty");
827 }
828 return false;
829 }
830
831 if (len >= 256) {
832 if (error_msg) {
833 char msg[256];
834 safe_snprintf(msg, sizeof(msg), "Invalid palette-chars: too long (%zu chars, max 255)", len);
835 *error_msg = platform_strdup(msg);
836 }
837 return false;
838 }
839
840 // Copy the palette characters
841 SAFE_STRNCPY(palette_custom, arg, 256);
842 palette_custom[255] = '\0';
843
844 // Also set the palette type to custom by calculating back to options_t pointer
845 // dest points to options_t.palette_custom, so we can get options_t* using offset arithmetic
846 options_t *opts = (options_t *)((char *)dest - offsetof(options_t, palette_custom));
847 opts->palette_type = PALETTE_CUSTOM;
848
849 return true;
850}
851
852bool parse_verbose_flag(const char *arg, void *dest, char **error_msg) {
853 (void)error_msg; // Unused but required by function signature
854
855 // If arg is NULL or starts with a flag, just increment
856 // Otherwise try to parse as integer count
857 unsigned short int *verbose_level = (unsigned short int *)dest;
858
859 if (!arg || arg[0] == '\0') {
860 // No argument provided, just increment
861 (*verbose_level)++;
862 return true;
863 }
864
865 // Try to parse as integer count
866 char *endptr;
867 long value = strtol(arg, &endptr, 10);
868 if (*endptr == '\0' && value >= 0 && value <= 100) {
869 *verbose_level = (unsigned short int)value;
870 return true;
871 }
872
873 // If it didn't parse as int, treat as flag increment
874 (*verbose_level)++;
875 return true;
876}
877
890bool parse_timestamp(const char *arg, void *dest, char **error_msg) {
891 if (!arg || arg[0] == '\0') {
892 if (error_msg) {
893 *error_msg = platform_strdup("--seek requires a timestamp argument");
894 }
895 return false;
896 }
897
898 double *timestamp = (double *)dest;
899 char *endptr;
900 long strtol_result;
901
902 // Count colons to determine format
903 int colon_count = 0;
904 for (const char *p = arg; *p; p++) {
905 if (*p == ':')
906 colon_count++;
907 }
908
909 if (colon_count == 0) {
910 // Plain seconds format: "30" or "30.5"
911 *timestamp = strtod(arg, &endptr);
912 if (*endptr != '\0' || *timestamp < 0.0) {
913 if (error_msg) {
914 *error_msg = platform_strdup("Invalid timestamp: expected non-negative seconds");
915 }
916 return false;
917 }
918 return true;
919 } else if (colon_count == 1) {
920 // MM:SS or MM:SS.ms format
921 strtol_result = strtol(arg, &endptr, 10);
922 if (*endptr != ':' || strtol_result < 0) {
923 if (error_msg) {
924 *error_msg = platform_strdup("Invalid timestamp: expected MM:SS or MM:SS.ms format");
925 }
926 return false;
927 }
928 long minutes = strtol_result;
929 double seconds = strtod(endptr + 1, &endptr);
930 if (*endptr != '\0' && *endptr != '.' && *endptr != '\0') {
931 if (error_msg) {
932 *error_msg = platform_strdup("Invalid timestamp: expected MM:SS or MM:SS.ms format");
933 }
934 return false;
935 }
936 *timestamp = minutes * 60.0 + seconds;
937 return true;
938 } else if (colon_count == 2) {
939 // HH:MM:SS or HH:MM:SS.ms format
940 strtol_result = strtol(arg, &endptr, 10);
941 if (*endptr != ':' || strtol_result < 0) {
942 if (error_msg) {
943 *error_msg = platform_strdup("Invalid timestamp: expected HH:MM:SS or HH:MM:SS.ms format");
944 }
945 return false;
946 }
947 long hours = strtol_result;
948
949 strtol_result = strtol(endptr + 1, &endptr, 10);
950 if (*endptr != ':' || strtol_result < 0 || strtol_result >= 60) {
951 if (error_msg) {
952 *error_msg = platform_strdup("Invalid timestamp: minutes must be 0-59");
953 }
954 return false;
955 }
956 long minutes = strtol_result;
957
958 double seconds = strtod(endptr + 1, &endptr);
959 if (*endptr != '\0') {
960 if (error_msg) {
961 *error_msg = platform_strdup("Invalid timestamp: expected HH:MM:SS or HH:MM:SS.ms format");
962 }
963 return false;
964 }
965 *timestamp = hours * (double)SEC_PER_HOUR + minutes * (double)SEC_PER_MIN + seconds;
966 return true;
967 } else {
968 if (error_msg) {
969 *error_msg = platform_strdup("Invalid timestamp format: too many colons");
970 }
971 return false;
972 }
973}
974
975bool parse_volume(const char *arg, void *dest, char **error_msg) {
976 if (!arg || !dest) {
977 if (error_msg) {
978 *error_msg = platform_strdup("Internal error: NULL argument or destination");
979 }
980 return false;
981 }
982
983 float *volume = (float *)dest;
984 char *endptr;
985 float val = strtof(arg, &endptr);
986
987 if (*endptr != '\0' || arg == endptr) {
988 if (error_msg) {
989 *error_msg = platform_strdup("Invalid volume value. Must be a number between 0.0 and 1.0");
990 }
991 return false;
992 }
993
994 if (val < 0.0f || val > 1.0f) {
995 if (error_msg) {
996 char buf[256];
997 SAFE_SNPRINTF(buf, sizeof(buf), "Volume must be between 0.0 and 1.0 (got %.2f)", val);
998 *error_msg = platform_strdup(buf);
999 }
1000 return false;
1001 }
1002
1003 *volume = val;
1004 return true;
1005}
1006
1007bool parse_log_file(const char *arg, void *dest, char **error_msg) {
1008 if (!arg || !dest) {
1009 if (error_msg) {
1010 *error_msg = platform_strdup("Internal error: NULL argument or destination");
1011 }
1012 return false;
1013 }
1014
1015 // Validate and normalize the log file path
1016 char *normalized = NULL;
1017 asciichat_error_t result = path_validate_user_path(arg, PATH_ROLE_LOG_FILE, &normalized);
1018
1019 if (result != ASCIICHAT_OK) {
1020 if (error_msg) {
1021 asciichat_error_context_t err_ctx;
1022 if (HAS_ERRNO(&err_ctx)) {
1023 *error_msg = platform_strdup(err_ctx.context_message);
1024 } else {
1025 *error_msg = platform_strdup("Log file path validation failed");
1026 }
1027 }
1028 return false;
1029 }
1030
1031 // Copy validated path to destination
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';
1036
1037 SAFE_FREE(normalized);
1038 return true;
1039}
1040
1041bool parse_audio_source(const char *arg, void *dest, char **error_msg) {
1042 if (!arg || !dest) {
1043 if (error_msg) {
1044 *error_msg = platform_strdup("Internal error: NULL argument or destination");
1045 }
1046 return false;
1047 }
1048
1049 audio_source_t *audio_source = (audio_source_t *)dest;
1050 char lower[32];
1051 to_lower(arg, lower, sizeof(lower));
1052
1053 // Auto (smart selection based on media state)
1054 if (strcmp(lower, "auto") == 0) {
1055 *audio_source = AUDIO_SOURCE_AUTO;
1056 return true;
1057 }
1058
1059 // Microphone only
1060 if (strcmp(lower, "mic") == 0) {
1061 *audio_source = AUDIO_SOURCE_MIC;
1062 return true;
1063 }
1064
1065 // Media only
1066 if (strcmp(lower, "media") == 0) {
1067 *audio_source = AUDIO_SOURCE_MEDIA;
1068 return true;
1069 }
1070
1071 // Both microphone and media
1072 if (strcmp(lower, "both") == 0) {
1073 *audio_source = AUDIO_SOURCE_BOTH;
1074 return true;
1075 }
1076
1077 if (error_msg) {
1078 *error_msg = platform_strdup("Audio source must be 'auto', 'mic', 'media', or 'both'");
1079 }
1080 return false;
1081}
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)
Definition ip.c:1320
int is_valid_ipv4(const char *ip)
Definition ip.c:58
int is_valid_ipv6(const char *ip)
Definition ip.c:105
int is_localhost_ipv4(const char *ip)
Definition ip.c:1299
int parse_ipv6_address(const char *input, char *output, size_t output_size)
Definition ip.c:158
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)
Definition parsers.c:300
int parse_server_bind_address(const char *arg, void *config, char **remaining, int num_remaining, char **error_msg)
Parse server bind address positional argument.
Definition parsers.c:512
bool parse_audio_source(const char *arg, void *dest, char **error_msg)
Definition parsers.c:1041
bool parse_port_option(const char *arg, void *dest, char **error_msg)
Definition parsers.c:472
bool parse_timestamp(const char *arg, void *dest, char **error_msg)
Custom parser for –seek flag.
Definition parsers.c:890
bool parse_palette_type(const char *arg, void *dest, char **error_msg)
Definition parsers.c:345
bool parse_verbose_flag(const char *arg, void *dest, char **error_msg)
Definition parsers.c:852
bool parse_color_filter(const char *arg, void *dest, char **error_msg)
Definition parsers.c:270
bool parse_color_setting(const char *arg, void *dest, char **error_msg)
Definition parsers.c:161
bool parse_log_file(const char *arg, void *dest, char **error_msg)
Definition parsers.c:1007
bool parse_volume(const char *arg, void *dest, char **error_msg)
Definition parsers.c:975
bool parse_log_level(const char *arg, void *dest, char **error_msg)
Definition parsers.c:408
int parse_client_address(const char *arg, void *config, char **remaining, int num_remaining, char **error_msg)
Parse client address positional argument.
Definition parsers.c:634
bool parse_color_mode(const char *arg, void *dest, char **error_msg)
Definition parsers.c:211
bool parse_utf8_setting(const char *arg, void *dest, char **error_msg)
Definition parsers.c:192
bool parse_palette_chars(const char *arg, void *dest, char **error_msg)
Definition parsers.c:809
asciichat_error_t parse_port(const char *str, uint16_t *out_port)
Definition parsing.c:251
asciichat_error_t path_validate_user_path(const char *input, path_role_t role, char **normalized_out)
Definition path.c:974
pcre2_code * asciichat_pcre2_singleton_get_code(pcre2_singleton_t *singleton)
Get the compiled pcre2_code from a singleton handle.
Definition pcre2.c:95
char * platform_strdup(const char *s)
Represents a thread-safe compiled PCRE2 regex singleton.
Definition pcre2.c:21
Lookup table for setting string-to-enum mapping.
Definition parsers.c:64
int enum_value
Corresponding enum value.
Definition parsers.c:66
const char * match
Setting string (lowercased)
Definition parsers.c:65
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456