ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
parsers.c
Go to the documentation of this file.
1
8#include <string.h>
9#include <stdlib.h>
10#include <ctype.h>
11#include <stddef.h>
12#include <ascii-chat/common.h>
15#include <ascii-chat/options/enums.h> // For OPT_* enum constants
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/rgba/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 = asciichat_pcre2_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// NOTE: is_session_string() is now imported from lib/discovery/strings.h
145// and provides enhanced validation against actual wordlists via hashtable lookup.
146// See that module for the full implementation.
147
148// Lookup table for color setting strings
149static const setting_map_entry_t g_color_setting_map[] = {
150 {"auto", COLOR_SETTING_AUTO}, {"a", COLOR_SETTING_AUTO},
151 {"0", COLOR_SETTING_AUTO}, {"true", COLOR_SETTING_TRUE},
152 {"yes", COLOR_SETTING_TRUE}, {"1", COLOR_SETTING_TRUE},
153 {"on", COLOR_SETTING_TRUE}, {"enabled", COLOR_SETTING_TRUE},
154 {"enable", COLOR_SETTING_TRUE}, {"false", COLOR_SETTING_FALSE},
156 {"off", COLOR_SETTING_FALSE}, {"disabled", COLOR_SETTING_FALSE},
157 {"disable", COLOR_SETTING_FALSE}, {NULL, 0} // Sentinel
158};
159
160bool parse_color_setting(const char *arg, void *dest, char **error_msg) {
161 if (!dest) {
162 if (error_msg) {
163 *error_msg = platform_strdup("Internal error: NULL destination");
164 }
165 return false;
166 }
167
168 // Use generic parser with color setting lookup table
169 // Default to TRUE if no arg provided
170 if (!arg || arg[0] == '\0') {
171 int *color_setting = (int *)dest;
172 *color_setting = COLOR_SETTING_TRUE;
173 return true;
174 }
175
176 return parse_setting_generic(arg, dest, g_color_setting_map, error_msg);
177}
178
179// Lookup table for UTF-8 setting strings (identical to color setting for boolean patterns)
180static const setting_map_entry_t g_utf8_setting_map[] = {
181 {"auto", UTF8_SETTING_AUTO}, {"a", UTF8_SETTING_AUTO},
182 {"0", UTF8_SETTING_AUTO}, {"true", UTF8_SETTING_TRUE},
183 {"yes", UTF8_SETTING_TRUE}, {"1", UTF8_SETTING_TRUE},
184 {"on", UTF8_SETTING_TRUE}, {"enabled", UTF8_SETTING_TRUE},
185 {"enable", UTF8_SETTING_TRUE}, {"false", UTF8_SETTING_FALSE},
186 {"no", UTF8_SETTING_FALSE}, {"-1", UTF8_SETTING_FALSE},
187 {"off", UTF8_SETTING_FALSE}, {"disabled", UTF8_SETTING_FALSE},
188 {"disable", UTF8_SETTING_FALSE}, {NULL, 0} // Sentinel
189};
190
191bool parse_utf8_setting(const char *arg, void *dest, char **error_msg) {
192 if (!dest) {
193 if (error_msg) {
194 *error_msg = platform_strdup("Internal error: NULL destination");
195 }
196 return false;
197 }
198
199 // Use generic parser with UTF-8 setting lookup table
200 // Default to TRUE if no arg provided
201 if (!arg || arg[0] == '\0') {
202 int *utf8_setting = (int *)dest;
203 *utf8_setting = UTF8_SETTING_TRUE;
204 return true;
205 }
206
207 return parse_setting_generic(arg, dest, g_utf8_setting_map, error_msg);
208}
209
210bool parse_color_mode(const char *arg, void *dest, char **error_msg) {
211 if (!arg || !dest) {
212 if (error_msg) {
213 *error_msg = platform_strdup("Internal error: NULL argument or destination");
214 }
215 return false;
216 }
217
218 terminal_color_mode_t *color_mode = (terminal_color_mode_t *)dest;
219 char lower[32];
220 to_lower(arg, lower, sizeof(lower));
221
222 // Auto-detect
223 if (strcmp(lower, OPT_COLOR_MODE_AUTO) == 0 || strcmp(lower, "a") == 0) {
224 *color_mode = TERM_COLOR_AUTO;
225 return true;
226 }
227
228 // Monochrome/None
229 if (strcmp(lower, OPT_COLOR_MODE_NONE) == 0 || strcmp(lower, "mono") == 0 || strcmp(lower, "monochrome") == 0 ||
230 strcmp(lower, "0") == 0) {
231 *color_mode = TERM_COLOR_NONE;
232 return true;
233 }
234
235 // 16-color
236 if (strcmp(lower, OPT_COLOR_MODE_16) == 0 || strcmp(lower, "16color") == 0 || strcmp(lower, "ansi") == 0 ||
237 strcmp(lower, "1") == 0) {
238 *color_mode = TERM_COLOR_16;
239 return true;
240 }
241
242 // 256-color
243 if (strcmp(lower, OPT_COLOR_MODE_256) == 0 || strcmp(lower, "256color") == 0 || strcmp(lower, "2") == 0) {
244 *color_mode = TERM_COLOR_256;
245 return true;
246 }
247
248 // Truecolor
249 if (strcmp(lower, OPT_COLOR_MODE_TRUECOLOR) == 0 || strcmp(lower, "true") == 0 || strcmp(lower, "tc") == 0 ||
250 strcmp(lower, "rgb") == 0 || strcmp(lower, "24bit") == 0 || strcmp(lower, "3") == 0) {
251 *color_mode = TERM_COLOR_TRUECOLOR;
252 return true;
253 }
254
255 // Invalid value - suggest closest match
256 if (error_msg) {
257 char msg[256];
258 const char *suggestion = asciichat_suggest_enum_value("color-mode", arg);
259 if (suggestion) {
260 safe_snprintf(msg, sizeof(msg), "Invalid color mode '%s'. Did you mean '%s'?", arg, suggestion);
261 } else {
262 safe_snprintf(msg, sizeof(msg), "Invalid color mode '%s'. Valid values: auto, none, 16, 256, truecolor", arg);
263 }
264 *error_msg = platform_strdup(msg);
265 }
266 return false;
267}
268
269bool parse_color_filter(const char *arg, void *dest, char **error_msg) {
270 if (!arg || !dest) {
271 if (error_msg) {
272 *error_msg = platform_strdup("Internal error: NULL argument or destination");
273 }
274 return false;
275 }
276
277 color_filter_t *color_filter = (color_filter_t *)dest;
278 char lower[32];
279 to_lower(arg, lower, sizeof(lower));
280
281 // Try to match against all known color filters
282 *color_filter = color_filter_from_cli_name(lower);
283 if (*color_filter != COLOR_FILTER_NONE || strcmp(lower, OPT_COLOR_FILTER_NONE) == 0) {
284 return true;
285 }
286
287 // Invalid value
288 if (error_msg) {
289 char msg[256];
290 safe_snprintf(msg, sizeof(msg),
291 "Invalid color filter '%s'. Valid values: none, black, white, green, magenta, fuchsia, "
292 "orange, teal, cyan, pink, red, yellow",
293 arg);
294 *error_msg = platform_strdup(msg);
295 }
296 return false;
297}
298
299bool parse_render_mode(const char *arg, void *dest, char **error_msg) {
300 if (!arg || !dest) {
301 if (error_msg) {
302 *error_msg = platform_strdup("Internal error: NULL argument or destination");
303 }
304 return false;
305 }
306
307 render_mode_t *render_mode = (render_mode_t *)dest;
308 char lower[32];
309 to_lower(arg, lower, sizeof(lower));
310
311 // Foreground mode
312 if (strcmp(lower, OPT_RENDER_MODE_FOREGROUND) == 0 || strcmp(lower, OPT_RENDER_MODE_FG) == 0 ||
313 strcmp(lower, "0") == 0) {
314 *render_mode = RENDER_MODE_FOREGROUND;
315 return true;
316 }
317
318 // Background mode
319 if (strcmp(lower, OPT_RENDER_MODE_BACKGROUND) == 0 || strcmp(lower, OPT_RENDER_MODE_BG) == 0 ||
320 strcmp(lower, "1") == 0) {
321 *render_mode = RENDER_MODE_BACKGROUND;
322 return true;
323 }
324
325 // Half-block mode
326 if (strcmp(lower, OPT_RENDER_MODE_HALF_BLOCK) == 0 || strcmp(lower, "hb") == 0 || strcmp(lower, "2") == 0) {
327 *render_mode = RENDER_MODE_HALF_BLOCK;
328 return true;
329 }
330
331 // Invalid value - suggest closest match
332 if (error_msg) {
333 char msg[256];
334 const char *suggestion = asciichat_suggest_enum_value("render-mode", arg);
335 if (suggestion) {
336 safe_snprintf(msg, sizeof(msg), "Invalid render mode '%s'. Did you mean '%s'?", arg, suggestion);
337 } else {
338 safe_snprintf(msg, sizeof(msg), "Invalid render mode '%s'. Valid values: foreground, background, half-block",
339 arg);
340 }
341 *error_msg = platform_strdup(msg);
342 }
343 return false;
344}
345
346bool parse_render_theme(const char *arg, void *dest, char **error_msg) {
347 if (!arg || !dest) {
348 if (error_msg) {
349 *error_msg = platform_strdup("Internal error: NULL argument or destination");
350 }
351 return false;
352 }
353
354 int *theme = (int *)dest;
355 char lower[32];
356 to_lower(arg, lower, sizeof(lower));
357
358 // Dark theme
359 if (strcmp(lower, "dark") == 0 || strcmp(lower, "0") == 0) {
360 *theme = 0; // TERM_RENDERER_THEME_DARK
361 return true;
362 }
363
364 // Light theme
365 if (strcmp(lower, "light") == 0 || strcmp(lower, "1") == 0) {
366 *theme = 1; // TERM_RENDERER_THEME_LIGHT
367 return true;
368 }
369
370 // Auto theme
371 if (strcmp(lower, "auto") == 0 || strcmp(lower, "2") == 0) {
372 *theme = 2; // TERM_RENDERER_THEME_AUTO
373 return true;
374 }
375
376 // Invalid value
377 if (error_msg) {
378 char msg[256];
379 safe_snprintf(msg, sizeof(msg), "Invalid render theme '%s'. Valid values: dark, light, auto", arg);
380 *error_msg = platform_strdup(msg);
381 }
382 return false;
383}
384
385bool parse_palette_type(const char *arg, void *dest, char **error_msg) {
386 if (!arg || !dest) {
387 if (error_msg) {
388 *error_msg = platform_strdup("Internal error: NULL argument or destination");
389 }
390 return false;
391 }
392
393 palette_type_t *palette_type = (palette_type_t *)dest;
394 char lower[32];
395 to_lower(arg, lower, sizeof(lower));
396
397 // Standard palette
398 if (strcmp(lower, OPT_PALETTE_STANDARD) == 0 || strcmp(lower, "std") == 0 || strcmp(lower, "0") == 0) {
399 *palette_type = PALETTE_STANDARD;
400 return true;
401 }
402
403 // Blocks palette
404 if (strcmp(lower, OPT_PALETTE_BLOCKS) == 0 || strcmp(lower, "block") == 0 || strcmp(lower, "1") == 0) {
405 *palette_type = PALETTE_BLOCKS;
406 return true;
407 }
408
409 // Digital palette
410 if (strcmp(lower, OPT_PALETTE_DIGITAL) == 0 || strcmp(lower, "dig") == 0 || strcmp(lower, "2") == 0) {
411 *palette_type = PALETTE_DIGITAL;
412 return true;
413 }
414
415 // Minimal palette
416 if (strcmp(lower, OPT_PALETTE_MINIMAL) == 0 || strcmp(lower, "min") == 0 || strcmp(lower, "3") == 0) {
417 *palette_type = PALETTE_MINIMAL;
418 return true;
419 }
420
421 // Cool palette
422 if (strcmp(lower, OPT_PALETTE_COOL) == 0 || strcmp(lower, "4") == 0) {
423 *palette_type = PALETTE_COOL;
424 return true;
425 }
426
427 // Custom palette
428 if (strcmp(lower, OPT_PALETTE_CUSTOM) == 0 || strcmp(lower, "5") == 0) {
429 *palette_type = PALETTE_CUSTOM;
430 return true;
431 }
432
433 // Invalid value - suggest closest match
434 if (error_msg) {
435 char msg[256];
436 const char *suggestion = asciichat_suggest_enum_value("palette", arg);
437 if (suggestion) {
438 safe_snprintf(msg, sizeof(msg), "Invalid palette type '%s'. Did you mean '%s'?", arg, suggestion);
439 } else {
440 safe_snprintf(msg, sizeof(msg),
441 "Invalid palette type '%s'. Valid values: standard, blocks, digital, minimal, cool, custom", arg);
442 }
443 *error_msg = platform_strdup(msg);
444 }
445 return false;
446}
447
448bool parse_log_level(const char *arg, void *dest, char **error_msg) {
449 if (!dest) {
450 if (error_msg) {
451 *error_msg = platform_strdup("Internal error: NULL destination");
452 }
453 return false;
454 }
455
456 log_level_t *log_level = (log_level_t *)dest;
457
458 // If no argument provided, use the default log level (based on build type)
459 if (!arg || arg[0] == '\0') {
460 *log_level = DEFAULT_LOG_LEVEL;
461 return true;
462 }
463
464 char lower[32];
465 to_lower(arg, lower, sizeof(lower));
466
467 // Development level
468 if (strcmp(lower, OPT_LOG_LEVEL_DEV) == 0 || strcmp(lower, "development") == 0 || strcmp(lower, "0") == 0) {
469 *log_level = LOG_DEV;
470 return true;
471 }
472
473 // Debug level
474 if (strcmp(lower, OPT_LOG_LEVEL_DEBUG) == 0 || strcmp(lower, "dbg") == 0 || strcmp(lower, "1") == 0) {
475 *log_level = LOG_DEBUG;
476 return true;
477 }
478
479 // Info level
480 if (strcmp(lower, OPT_LOG_LEVEL_INFO) == 0 || strcmp(lower, "information") == 0 || strcmp(lower, "2") == 0) {
481 *log_level = LOG_INFO;
482 return true;
483 }
484
485 // Warning level
486 if (strcmp(lower, OPT_LOG_LEVEL_WARN) == 0 || strcmp(lower, "warning") == 0 || strcmp(lower, "3") == 0) {
487 *log_level = LOG_WARN;
488 return true;
489 }
490
491 // Error level
492 if (strcmp(lower, OPT_LOG_LEVEL_ERROR) == 0 || strcmp(lower, "err") == 0 || strcmp(lower, "4") == 0) {
493 *log_level = LOG_ERROR;
494 return true;
495 }
496
497 // Fatal level
498 if (strcmp(lower, OPT_LOG_LEVEL_FATAL) == 0 || strcmp(lower, "5") == 0) {
499 *log_level = LOG_FATAL;
500 return true;
501 }
502
503 // Invalid value
504 if (error_msg) {
505 char msg[256];
506 safe_snprintf(msg, sizeof(msg), "Invalid log level '%s'. Valid values: dev, debug, info, warn, error, fatal", arg);
507 *error_msg = platform_strdup(msg);
508 }
509 return false;
510}
511
512bool parse_port_option(const char *arg, void *dest, char **error_msg) {
513 if (!arg || !dest) {
514 if (error_msg) {
515 *error_msg = platform_strdup("Internal error: NULL argument or destination");
516 }
517 return false;
518 }
519
520 int *port_value = (int *)dest;
521 uint16_t port_num;
522
523 // Use the existing parse_port function for validation
524 asciichat_error_t err = parse_port(arg, &port_num);
525 if (err != ASCIICHAT_OK) {
526 if (error_msg) {
527 char msg[256];
528 safe_snprintf(msg, sizeof(msg), "Invalid port '%s'. Port must be a number between 0 and 65535 (0 = auto-assign).",
529 arg);
530 *error_msg = platform_strdup(msg);
531 }
532 return false;
533 }
534
535 // Port zero is useful to low-level socket APIs for ephemeral binding, but
536 // it is not a valid ascii-chat user-facing port and conflicts with the
537 // option registry's 1-65535 contract.
538 if (port_num == 0) {
539 if (error_msg) {
540 *error_msg = platform_strdup("Port must be between 1 and 65535");
541 }
542 return false;
543 }
544
545 *port_value = (int)port_num;
546 return true;
547}
548
549// ============================================================================
550// Positional Argument Parsers
551// ============================================================================
552
553#include <ascii-chat/util/ip.h>
554#include <string.h>
555
563int parse_server_bind_address(const char *arg, void *config, char **remaining, int num_remaining, char **error_msg) {
564 if (!arg || !config) {
565 if (error_msg) {
566 *error_msg = platform_strdup("Internal error: NULL argument or config");
567 }
568 return -1;
569 }
570
571 // Assume config struct has address and address6 fields (OPTIONS_BUFF_SIZE each)
572 // This is a simplified version that assumes standard options_state layout
573 char *address = (char *)config + offsetof(struct options_state, address);
574 char *address6 = (char *)config + offsetof(struct options_state, address6);
575
576 int consumed = 0;
577
578 // Parse first argument (IPv4 or IPv6)
579 char parsed_addr[OPTIONS_BUFF_SIZE];
580 const char *addr_to_check = arg;
581 if (parse_ipv6_address(arg, parsed_addr, sizeof(parsed_addr)) == 0) {
582 addr_to_check = parsed_addr;
583 }
584
585 // Check if it's IPv4 or IPv6
586 if (is_valid_ipv4(addr_to_check)) {
587 // Check if we already have a non-default IPv4 address
588 // Allow overwriting defaults (localhost, 0.0.0.0)
589 if (address[0] != '\0' && !is_localhost_ipv4(address) && strcmp(address, "localhost") != 0 &&
590 strcmp(address, "0.0.0.0") != 0) {
591 if (error_msg) {
592 char msg[256];
593 safe_snprintf(msg, sizeof(msg),
594 "Cannot specify multiple IPv4 addresses.\n"
595 "Already have: %s\n"
596 "Cannot add: %s",
597 address, addr_to_check);
598 *error_msg = platform_strdup(msg);
599 }
600 return -1;
601 }
602 SAFE_SNPRINTF(address, OPTIONS_BUFF_SIZE, "%s", addr_to_check);
603 consumed = 1;
604 } else if (is_valid_ipv6(addr_to_check)) {
605 // Check if we already have a non-default IPv6 address
606 // Allow overwriting default (::1)
607 if (address6[0] != '\0' && !is_localhost_ipv6(address6)) {
608 if (error_msg) {
609 char msg[256];
610 safe_snprintf(msg, sizeof(msg),
611 "Cannot specify multiple IPv6 addresses.\n"
612 "Already have: %s\n"
613 "Cannot add: %s",
614 address6, addr_to_check);
615 *error_msg = platform_strdup(msg);
616 }
617 return -1;
618 }
619 SAFE_SNPRINTF(address6, OPTIONS_BUFF_SIZE, "%s", addr_to_check);
620 consumed = 1;
621 } else {
622 if (error_msg) {
623 char msg[512];
624 safe_snprintf(msg, sizeof(msg),
625 "Invalid IP address '%s'.\n"
626 "Server bind addresses must be valid IPv4 or IPv6 addresses.\n"
627 "Examples:\n"
628 " ascii-chat server 0.0.0.0\n"
629 " ascii-chat server ::1\n"
630 " ascii-chat server 0.0.0.0 ::1",
631 arg);
632 *error_msg = platform_strdup(msg);
633 }
634 return -1;
635 }
636
637 // Try to parse second address if available
638 if (remaining && num_remaining > 0 && remaining[0]) {
639 const char *second_arg = remaining[0];
640 memset(parsed_addr, 0, sizeof(parsed_addr));
641 addr_to_check = second_arg;
642 if (parse_ipv6_address(second_arg, parsed_addr, sizeof(parsed_addr)) == 0) {
643 addr_to_check = parsed_addr;
644 }
645
646 if (is_valid_ipv4(addr_to_check)) {
647 // Second is IPv4
648 if (address[0] != '\0' && !is_localhost_ipv4(address) && strcmp(address, "localhost") != 0 &&
649 strcmp(address, "0.0.0.0") != 0) {
650 // Already have an IPv4, can't add another
651 return consumed;
652 }
653 if (is_valid_ipv4(arg)) {
654 // First was also IPv4, can't have two IPv4s
655 return consumed;
656 }
657 // First was IPv6, second is IPv4 - accept both
658 SAFE_SNPRINTF(address, OPTIONS_BUFF_SIZE, "%s", addr_to_check);
659 consumed = 2;
660 } else if (is_valid_ipv6(addr_to_check)) {
661 // Second is IPv6
662 if (address6[0] != '\0' && !is_localhost_ipv6(address6)) {
663 // Already have an IPv6, can't add another
664 return consumed;
665 }
666 if (is_valid_ipv6(arg)) {
667 // First was also IPv6, can't have two IPv6s
668 return consumed;
669 }
670 // First was IPv4, second is IPv6 - accept both
671 SAFE_SNPRINTF(address6, OPTIONS_BUFF_SIZE, "%s", addr_to_check);
672 consumed = 2;
673 }
674 }
675
676 return consumed;
677}
678
685int parse_client_address(const char *arg, void *config, char **remaining, int num_remaining, char **error_msg) {
686 (void)remaining;
687 (void)num_remaining;
688
689 if (!arg || !config) {
690 if (error_msg) {
691 *error_msg = platform_strdup("Internal error: NULL argument or config");
692 }
693 return -1;
694 }
695
696 log_debug("parse_client_address: Processing argument: '%s'", arg);
697
698 // Access address and port fields from options_state struct
699 char *address = (char *)config + offsetof(struct options_state, address);
700 int *port = (int *)((char *)config + offsetof(struct options_state, port));
701
702 // Check for WebSocket URL (ws:// or wss://) FIRST before session string validation
703 // WebSocket URLs are passed through without validation or port extraction
704 if (strncmp(arg, "ws://", 5) == 0 || strncmp(arg, "wss://", 6) == 0) {
705 log_debug("Detected WebSocket URL: %s", arg);
706 SAFE_SNPRINTF(address, OPTIONS_BUFF_SIZE, "%s", arg);
707 // For wss://, TLS handles transport encryption - allow ACIP to handle auth independently
708 // For ws://, default to ACIP encryption (no TLS)
709 if (strncmp(arg, "wss://", 6) == 0) {
710 bool *no_encrypt = (bool *)config + offsetof(struct options_state, no_encrypt);
711 *no_encrypt = true;
712 log_debug("Auto-detected wss:// - setting no_encrypt=true (TLS handles transport encryption, ACIP handles auth)");
713 }
714 // Don't set port - WebSocket transport handles URL parsing internally
715 return 1; // Consumed 1 argument
716 }
717
718 // Check for TCP URL (tcp://) - passed through directly to FFmpeg
719 // TCP URLs are used for streaming media from custom TCP servers
720 if (strncmp(arg, "tcp://", 6) == 0) {
721 log_debug("Detected TCP URL: %s", arg);
722 SAFE_SNPRINTF(address, OPTIONS_BUFF_SIZE, "%s", arg);
723 // Don't set port - TCP transport handles URL parsing internally
724 return 1; // Consumed 1 argument
725 }
726
727 // Check if this is a session string (format: adjective-noun-noun)
728 // Session strings have exactly 2 hyphens, only lowercase letters, length 5-47
729 bool is_session = is_session_string(arg);
730 log_debug("parse_client_address: is_session_string('%s') = %s", arg, is_session ? "true" : "false");
731
732 if (is_session) {
733 // This is a session string, not a server address
734 char *session_string = (char *)config + offsetof(struct options_state, session_string);
735 SAFE_SNPRINTF(session_string, SESSION_STRING_BUFFER_SIZE, "%s", arg);
736 log_debug("parse_client_address: Stored session string: %s", arg);
737 return 1; // Consumed 1 arg
738 }
739
740 // Not a session string, parse as server address
741 log_debug("parse_client_address: Parsing as server address (not a session string)");
742
743 // Check for port in address (format: address:port or [ipv6]:port)
744 const char *colon = strrchr(arg, ':');
745
746 if (colon != NULL) {
747 // Check if this is IPv6 with port [::1]:port or plain hostname:port
748 if (arg[0] == '[') {
749 // IPv6 with brackets: [address]:port
750 const char *closing_bracket = strchr(arg, ']');
751 if (closing_bracket && closing_bracket < colon) {
752 // Extract address (remove brackets)
753 size_t addr_len = (size_t)(closing_bracket - arg - 1);
754 if (addr_len >= OPTIONS_BUFF_SIZE) {
755 if (error_msg) {
756 *error_msg = platform_strdup("IPv6 address too long");
757 }
758 return -1;
759 }
760 SAFE_SNPRINTF(address, OPTIONS_BUFF_SIZE, "%.*s", (int)addr_len, arg + 1);
761
762 // Extract and validate port
763 const char *port_str = colon + 1;
764 char *endptr;
765 long port_num = strtol(port_str, &endptr, 10);
766 if (*endptr != '\0' || port_num < 1 || port_num > 65535) {
767 if (error_msg) {
768 char msg[256];
769 safe_snprintf(msg, sizeof(msg), "Invalid port number '%s'. Must be 1-65535.", port_str);
770 *error_msg = platform_strdup(msg);
771 }
772 return -1;
773 }
774 *port = (int)port_num;
775 }
776 } else {
777 // Check if it's IPv6 without brackets (no port allowed)
778 // or hostname/IPv4:port
779 size_t colon_count = 0;
780 for (const char *p = arg; *p; p++) {
781 if (*p == ':')
782 colon_count++;
783 }
784
785 if (colon_count == 1) {
786 // Likely hostname:port or IPv4:port
787 size_t addr_len = (size_t)(colon - arg);
788 if (addr_len >= OPTIONS_BUFF_SIZE) {
789 if (error_msg) {
790 *error_msg = platform_strdup("Address too long");
791 }
792 return -1;
793 }
794 SAFE_SNPRINTF(address, OPTIONS_BUFF_SIZE, "%.*s", (int)addr_len, arg);
795
796 // Extract and validate port
797 const char *port_str = colon + 1;
798 char *endptr;
799 long port_num = strtol(port_str, &endptr, 10);
800 if (*endptr != '\0' || port_num < 1 || port_num > 65535) {
801 if (error_msg) {
802 char msg[256];
803 safe_snprintf(msg, sizeof(msg), "Invalid port number '%s'. Must be 1-65535.", port_str);
804 *error_msg = platform_strdup(msg);
805 }
806 return -1;
807 }
808 *port = (int)port_num;
809 } else {
810 // Multiple colons - likely bare IPv6 address
811 SAFE_SNPRINTF(address, OPTIONS_BUFF_SIZE, "%s", arg);
812 }
813 }
814 } else {
815 // No colon - just an address
816 SAFE_SNPRINTF(address, OPTIONS_BUFF_SIZE, "%s", arg);
817 }
818
819 // Validate addresses using comprehensive IPv4/IPv6 detection
820 bool has_dot = strchr(address, '.') != NULL;
821 bool has_colon = strchr(address, ':') != NULL;
822 bool starts_with_digit = address[0] >= '0' && address[0] <= '9';
823
824 // Potential IPv6 address (has colons) - validate as IPv6
825 if (has_colon) {
826 if (!is_valid_ipv6(address)) {
827 if (error_msg) {
828 char msg[512];
829 safe_snprintf(msg, sizeof(msg),
830 "Invalid IPv6 address '%s'.\n"
831 "IPv6 addresses must be valid hex notation with colons.\n"
832 "Examples: ::1, 2001:db8::1, fe80::1\n"
833 "Or use hostnames like example.com",
834 address);
835 *error_msg = platform_strdup(msg);
836 }
837 return -1;
838 }
839 } else if (has_dot && starts_with_digit) {
840 // Potential IPv4 address (has dots and starts with digit) - validate strictly
841 if (!is_valid_ipv4(address)) {
842 if (error_msg) {
843 char msg[512];
844 safe_snprintf(msg, sizeof(msg),
845 "Invalid IPv4 address '%s'.\n"
846 "IPv4 addresses must have exactly 4 octets (0-255) separated by dots.\n"
847 "Examples: 127.0.0.1, 192.168.1.1\n"
848 "For hostnames, use letters: example.com, localhost",
849 address);
850 *error_msg = platform_strdup(msg);
851 }
852 return -1;
853 }
854 }
855 // Otherwise treat as valid hostname (no validation needed)
856
857 // Note: Port conflict checking would require additional state
858 // (checking if --port flag was used). For now, this is a simplified version.
859 // Full implementation would need to track whether port was set via flag.
860
861 log_debug("parse_client_address: Set address='%s', port=%d", address[0] ? address : "(empty)", *port);
862
863 return 1; // Consumed 1 arg
864}
865
866// ============================================================================
867// Mirror Mode Media Positional Argument Parser
868// ============================================================================
869
879int parse_mirror_media(const char *arg, void *config, char **remaining, int num_remaining, char **error_msg) {
880 (void)remaining;
881 (void)num_remaining;
882
883 if (!arg || !config) {
884 if (error_msg) {
885 *error_msg = platform_strdup("Internal error: NULL argument or config");
886 }
887 return -1;
888 }
889
890 log_debug("parse_mirror_media: Processing argument: '%s'", arg);
891
892 // Access media_file and media_url fields from options_state struct (same as other positional parsers)
893 char *media_file = (char *)config + offsetof(struct options_state, media_file);
894 char *media_url = (char *)config + offsetof(struct options_state, media_url);
895
896 // Enforce mutual exclusion with --file flag
897 if (media_file[0] != '\0') {
898 if (error_msg) {
899 *error_msg = platform_strdup("cannot use both --file and a positional argument");
900 }
901 return -1;
902 }
903
904 // Enforce mutual exclusion with --url flag
905 if (media_url[0] != '\0') {
906 if (error_msg) {
907 *error_msg = platform_strdup("cannot use both --url and a positional argument");
908 }
909 return -1;
910 }
911
912 // Check if it's an accessible file path (priority 1)
913 if (platform_access(arg, PLATFORM_ACCESS_READ) == 0) {
914 log_debug("Argument '%s' is an accessible file", arg);
915 SAFE_SNPRINTF(media_file, OPTIONS_BUFF_SIZE, "%s", arg);
916 return 1; // Consumed 1 argument
917 }
918
919 // Not a file; treat as URL (yt-dlp/ffmpeg will handle any errors)
920 log_debug("Argument '%s' not found as file; treating as URL", arg);
921 SAFE_SNPRINTF(media_url, OPTIONS_BUFF_SIZE, "%s", arg);
922 return 1; // Consumed 1 argument
923}
924
925// ============================================================================
926// Palette Characters Parser
927// ============================================================================
928
929bool parse_palette_chars(const char *arg, void *dest, char **error_msg) {
930 if (!arg || !dest) {
931 if (error_msg) {
932 *error_msg = platform_strdup("Internal error: NULL argument or destination");
933 }
934 return false;
935 }
936
937 // The dest pointer points to the palette_custom field in options_t
938 // We need to get the full options_t struct to call parse_palette_chars_option
939 // Since we only have the field pointer, we need to handle this directly
940
941 char *palette_custom = (char *)dest;
942
943 size_t len = strlen(arg);
944 if (len == 0) {
945 if (error_msg) {
946 *error_msg = platform_strdup("Invalid palette-chars: value cannot be empty");
947 }
948 return false;
949 }
950
951 if (len >= 256) {
952 if (error_msg) {
953 char msg[256];
954 safe_snprintf(msg, sizeof(msg), "Invalid palette-chars: too long (%zu chars, max 255)", len);
955 *error_msg = platform_strdup(msg);
956 }
957 return false;
958 }
959
960 // Copy the palette characters
961 SAFE_STRNCPY(palette_custom, arg, 256);
962 palette_custom[255] = '\0';
963
964 // Also set the palette type to custom by calculating back to options_t pointer
965 // dest points to options_t.palette_custom, so we can get options_t* using offset arithmetic
966 options_t *opts = (options_t *)((char *)dest - offsetof(options_t, palette_custom));
968
969 return true;
970}
971
972bool parse_verbose_flag(const char *arg, void *dest, char **error_msg) {
973 (void)error_msg; // Unused but required by function signature
974
975 // If arg is NULL or starts with a flag, just increment
976 // Otherwise try to parse as integer count
977 unsigned short int *verbose_level = (unsigned short int *)dest;
978
979 if (!arg || arg[0] == '\0') {
980 // No argument provided, just increment
981 (*verbose_level)++;
982 return true;
983 }
984
985 // Try to parse as integer count
986 char *endptr;
987 long value = strtol(arg, &endptr, 10);
988 if (*endptr == '\0' && value >= 0 && value <= 100) {
989 *verbose_level = (unsigned short int)value;
990 return true;
991 }
992
993 // If it didn't parse as int, treat as flag increment
994 (*verbose_level)++;
995 return true;
996}
997
1010bool parse_timestamp(const char *arg, void *dest, char **error_msg) {
1011 if (!arg || arg[0] == '\0') {
1012 if (error_msg) {
1013 *error_msg = platform_strdup("--seek requires a timestamp argument");
1014 }
1015 return false;
1016 }
1017
1018 double *timestamp = (double *)dest;
1019 char *endptr;
1020 long strtol_result;
1021
1022 // Count colons to determine format
1023 int colon_count = 0;
1024 for (const char *p = arg; *p; p++) {
1025 if (*p == ':')
1026 colon_count++;
1027 }
1028
1029 if (colon_count == 0) {
1030 // Plain seconds format: "30" or "30.5"
1031 *timestamp = strtod(arg, &endptr);
1032 if (*endptr != '\0' || *timestamp < 0.0) {
1033 if (error_msg) {
1034 *error_msg = platform_strdup("Invalid timestamp: expected non-negative seconds");
1035 }
1036 return false;
1037 }
1038 return true;
1039 } else if (colon_count == 1) {
1040 // MM:SS or MM:SS.ms format
1041 strtol_result = strtol(arg, &endptr, 10);
1042 if (*endptr != ':' || strtol_result < 0) {
1043 if (error_msg) {
1044 *error_msg = platform_strdup("Invalid timestamp: expected MM:SS or MM:SS.ms format");
1045 }
1046 return false;
1047 }
1048 long minutes = strtol_result;
1049 double seconds = strtod(endptr + 1, &endptr);
1050 if (*endptr != '\0' && *endptr != '.' && *endptr != '\0') {
1051 if (error_msg) {
1052 *error_msg = platform_strdup("Invalid timestamp: expected MM:SS or MM:SS.ms format");
1053 }
1054 return false;
1055 }
1056 *timestamp = minutes * 60.0 + seconds;
1057 return true;
1058 } else if (colon_count == 2) {
1059 // HH:MM:SS or HH:MM:SS.ms format
1060 strtol_result = strtol(arg, &endptr, 10);
1061 if (*endptr != ':' || strtol_result < 0) {
1062 if (error_msg) {
1063 *error_msg = platform_strdup("Invalid timestamp: expected HH:MM:SS or HH:MM:SS.ms format");
1064 }
1065 return false;
1066 }
1067 long hours = strtol_result;
1068
1069 strtol_result = strtol(endptr + 1, &endptr, 10);
1070 if (*endptr != ':' || strtol_result < 0 || strtol_result >= 60) {
1071 if (error_msg) {
1072 *error_msg = platform_strdup("Invalid timestamp: minutes must be 0-59");
1073 }
1074 return false;
1075 }
1076 long minutes = strtol_result;
1077
1078 double seconds = strtod(endptr + 1, &endptr);
1079 if (*endptr != '\0') {
1080 if (error_msg) {
1081 *error_msg = platform_strdup("Invalid timestamp: expected HH:MM:SS or HH:MM:SS.ms format");
1082 }
1083 return false;
1084 }
1085 *timestamp = hours * (double)SEC_PER_HOUR + minutes * (double)SEC_PER_MIN + seconds;
1086 return true;
1087 } else {
1088 if (error_msg) {
1089 *error_msg = platform_strdup("Invalid timestamp format: too many colons");
1090 }
1091 return false;
1092 }
1093}
1094
1095bool parse_volume(const char *arg, void *dest, char **error_msg) {
1096 if (!arg || !dest) {
1097 if (error_msg) {
1098 *error_msg = platform_strdup("Internal error: NULL argument or destination");
1099 }
1100 return false;
1101 }
1102
1103 float *volume = (float *)dest;
1104 char *endptr;
1105 float val = strtof(arg, &endptr);
1106
1107 if (*endptr != '\0' || arg == endptr) {
1108 if (error_msg) {
1109 *error_msg = platform_strdup("Invalid volume value. Must be a number between 0.0 and 1.0");
1110 }
1111 return false;
1112 }
1113
1114 if (val < 0.0f || val > 1.0f) {
1115 if (error_msg) {
1116 char buf[256];
1117 SAFE_SNPRINTF(buf, sizeof(buf), "Volume must be between 0.0 and 1.0 (got %.2f)", val);
1118 *error_msg = platform_strdup(buf);
1119 }
1120 return false;
1121 }
1122
1123 *volume = val;
1124 return true;
1125}
1126
1127bool parse_log_file(const char *arg, void *dest, char **error_msg) {
1128 if (!arg || !dest) {
1129 if (error_msg) {
1130 *error_msg = platform_strdup("Internal error: NULL argument or destination");
1131 }
1132 return false;
1133 }
1134
1135 // Validate and normalize the log file path
1136 char *normalized = NULL;
1138
1139 if (result != ASCIICHAT_OK) {
1140 if (error_msg) {
1142 if (HAS_ERRNO(&err_ctx)) {
1143 *error_msg = platform_strdup(err_ctx.context_message);
1144 } else {
1145 *error_msg = platform_strdup("Log file path validation failed");
1146 }
1147 }
1148 return false;
1149 }
1150
1151 // Copy validated path to destination
1152 char *log_file_buf = (char *)dest;
1153 const size_t max_size = 256;
1154 SAFE_STRNCPY(log_file_buf, normalized, max_size - 1);
1155 log_file_buf[max_size - 1] = '\0';
1156
1157 SAFE_FREE(normalized);
1158 return true;
1159}
1160
1161bool parse_audio_source(const char *arg, void *dest, char **error_msg) {
1162 if (!arg || !dest) {
1163 if (error_msg) {
1164 *error_msg = platform_strdup("Internal error: NULL argument or destination");
1165 }
1166 return false;
1167 }
1168
1169 audio_source_t *audio_source = (audio_source_t *)dest;
1170 char lower[32];
1171 to_lower(arg, lower, sizeof(lower));
1172
1173 if (strcmp(lower, OPT_AUDIO_SOURCE_ALL) == 0 || strcmp(lower, OPT_AUDIO_SOURCE_AUTO) == 0 ||
1174 strcmp(lower, "both") == 0) {
1175 *audio_source = AUDIO_SOURCE_ALL;
1176 return true;
1177 }
1178 if (strcmp(lower, OPT_AUDIO_SOURCE_CALL) == 0 || strcmp(lower, "remote") == 0) {
1179 *audio_source = AUDIO_SOURCE_CALL;
1180 return true;
1181 }
1182 if (strcmp(lower, OPT_AUDIO_SOURCE_MIC) == 0) {
1183 *audio_source = AUDIO_SOURCE_MIC;
1184 return true;
1185 }
1186 if (strcmp(lower, OPT_AUDIO_SOURCE_MEDIA) == 0) {
1187 *audio_source = AUDIO_SOURCE_MEDIA;
1188 return true;
1189 }
1190
1191 if (error_msg) {
1192 *error_msg = platform_strdup("Audio source must be 'all', 'call', 'mic', or 'media'");
1193 }
1194 return false;
1195}
1196
1197bool parse_audio_capture_source(const char *arg, void *dest, char **error_msg) {
1198 if (!arg || !dest) {
1199 if (error_msg)
1200 *error_msg = platform_strdup("Internal error: NULL argument or destination");
1201 return false;
1202 }
1203
1205 char lower[32];
1206 to_lower(arg, lower, sizeof(lower));
1207 if (strcmp(lower, OPT_AUDIO_CAPTURE_SOURCE_AUTO) == 0)
1208 *source = AUDIO_CAPTURE_SOURCE_AUTO;
1209 else if (strcmp(lower, OPT_AUDIO_CAPTURE_SOURCE_MIC) == 0)
1210 *source = AUDIO_CAPTURE_SOURCE_MIC;
1211 else if (strcmp(lower, OPT_AUDIO_CAPTURE_SOURCE_MEDIA) == 0)
1213 else if (strcmp(lower, OPT_AUDIO_CAPTURE_SOURCE_BOTH) == 0)
1214 *source = AUDIO_CAPTURE_SOURCE_BOTH;
1215 else if (strcmp(lower, OPT_AUDIO_CAPTURE_SOURCE_REMOTE) == 0)
1217 else {
1218 if (error_msg)
1219 *error_msg = platform_strdup("Audio capture source must be 'auto', 'mic', 'media', 'both', or 'remote'");
1220 return false;
1221 }
1222 return true;
1223}
Options builder API for flexible command-line option configuration.
⚙️ Common definitions, error codes, macros, and types shared throughout the application
bool is_session_string(const char *str)
Check if a string is a valid session string.
Option enum value definitions - single source of truth.
#define OPT_PALETTE_CUSTOM
Definition enums.h:110
#define OPT_AUDIO_SOURCE_CALL
Definition enums.h:125
#define OPT_LOG_LEVEL_DEV
Definition enums.h:75
#define OPT_AUDIO_SOURCE_AUTO
Definition enums.h:128
#define OPT_PALETTE_COOL
Definition enums.h:109
#define OPT_RENDER_MODE_BG
Definition enums.h:116
#define OPT_RENDER_MODE_FOREGROUND
Definition enums.h:113
#define OPT_AUDIO_CAPTURE_SOURCE_BOTH
Definition enums.h:134
#define OPT_AUDIO_CAPTURE_SOURCE_AUTO
Definition enums.h:131
#define OPT_AUDIO_CAPTURE_SOURCE_MEDIA
Definition enums.h:133
#define OPT_LOG_LEVEL_FATAL
Definition enums.h:80
#define OPT_LOG_LEVEL_ERROR
Definition enums.h:79
#define OPT_RENDER_MODE_HALF_BLOCK
Definition enums.h:117
#define OPT_COLOR_MODE_256
Definition enums.h:86
#define OPT_AUDIO_SOURCE_ALL
Definition enums.h:124
#define OPT_RENDER_MODE_FG
Definition enums.h:114
#define OPT_LOG_LEVEL_INFO
Definition enums.h:77
#define OPT_LOG_LEVEL_DEBUG
Definition enums.h:76
#define OPT_COLOR_MODE_NONE
Definition enums.h:84
#define OPT_PALETTE_MINIMAL
Definition enums.h:108
#define OPT_AUDIO_CAPTURE_SOURCE_REMOTE
Definition enums.h:135
#define OPT_PALETTE_BLOCKS
Definition enums.h:106
#define OPT_COLOR_FILTER_NONE
Definition enums.h:90
#define OPT_AUDIO_CAPTURE_SOURCE_MIC
Definition enums.h:132
#define OPT_AUDIO_SOURCE_MEDIA
Definition enums.h:127
#define OPT_PALETTE_STANDARD
Definition enums.h:105
#define OPT_COLOR_MODE_TRUECOLOR
Definition enums.h:87
#define OPT_PALETTE_DIGITAL
Definition enums.h:107
#define OPT_LOG_LEVEL_WARN
Definition enums.h:78
#define OPT_RENDER_MODE_BACKGROUND
Definition enums.h:115
#define OPT_COLOR_MODE_AUTO
Definition enums.h:83
#define OPT_COLOR_MODE_16
Definition enums.h:85
#define OPT_AUDIO_SOURCE_MIC
Definition enums.h:126
unsigned short uint16_t
Definition common.h:57
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_SNPRINTF(buffer, buffer_size,...)
Definition common.h:492
#define HAS_ERRNO(var)
Check if an error occurred and get full context.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ASCIICHAT_OK
Definition error_codes.h:51
log_level_t
Logging levels enumeration.
Definition types.h:29
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
#define DEFAULT_LOG_LEVEL
Default log level for debug builds (DEBUG and above)
Definition log/log.h:65
#define SEC_PER_HOUR
Definition time.h:168
#define SEC_PER_MIN
Definition time.h:167
#define OPTIONS_BUFF_SIZE
Buffer size for option string values.
audio_source_t
Audio source selected for visualization.
@ UTF8_SETTING_AUTO
Smart detection (default): UTF-8 if terminal supports it.
@ UTF8_SETTING_FALSE
Force UTF-8 OFF: disable UTF-8 support.
@ UTF8_SETTING_TRUE
Force UTF-8 ON: always use UTF-8 regardless of terminal capability.
@ COLOR_SETTING_TRUE
Force colors ON: always colorize regardless of TTY/piping/CLAUDECODE.
@ COLOR_SETTING_FALSE
Force colors OFF: disable all colors and logging colors.
@ COLOR_SETTING_AUTO
Smart detection (default): colors if TTY and not CLAUDECODE and not piping.
palette_type_t
Built-in palette type enumeration.
Definition palette.h:84
@ PALETTE_BLOCKS
Unicode block characters: " ░░▒▒▓▓██".
Definition palette.h:90
@ PALETTE_CUSTOM
User-defined via –palette-chars.
Definition palette.h:98
@ PALETTE_COOL
Ascending blocks: " ▁▂▃▄▅▆▇█".
Definition palette.h:96
@ PALETTE_STANDARD
Standard ASCII palette: " ...',;:clodxkO0KXNWM".
Definition palette.h:88
@ PALETTE_DIGITAL
Digital/glitch aesthetic: " -=≡≣▰▱◼".
Definition palette.h:92
@ PALETTE_MINIMAL
Simple ASCII: " .-+*#".
Definition palette.h:94
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
int platform_access(const char *pathname, int mode)
Check file/directory access permissions.
char * platform_strdup(const char *s)
Duplicate string (strdup replacement)
#define PLATFORM_ACCESS_READ
Check if file/directory is readable.
int is_localhost_ipv6(const char *ip)
Check if IPv6 address is localhost.
Definition ip.c:1320
int is_valid_ipv4(const char *ip)
Check if a string is a valid IPv4 address.
Definition ip.c:58
int is_valid_ipv6(const char *ip)
Check if a string is a valid IPv6 address.
Definition ip.c:105
int is_localhost_ipv4(const char *ip)
Check if IPv4 address is localhost.
Definition ip.c:1299
asciichat_error_t path_validate_user_path(const char *input, path_role_t role, char **normalized_out)
Validate and canonicalize a user-supplied filesystem path.
Definition path.c:1020
int parse_ipv6_address(const char *input, char *output, size_t output_size)
Parse IPv6 address, removing brackets if present.
Definition ip.c:158
@ PATH_ROLE_LOG_FILE
Definition path.h:318
color_filter_t color_filter_from_cli_name(const char *cli_name)
Convert color filter CLI name to enum value.
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
🌍 IP Address Parsing and Formatting Utilities
const char * asciichat_suggest_enum_value(const char *option_name, const char *input)
Suggest closest matching enum value using fuzzy matching.
Centralized enum and mode string conversion API.
bool parse_render_mode(const char *arg, void *dest, char **error_msg)
Parse render mode option.
Definition parsers.c:299
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:563
bool parse_audio_source(const char *arg, void *dest, char **error_msg)
Parse visualization source option (–audio-source)
Definition parsers.c:1161
bool parse_port_option(const char *arg, void *dest, char **error_msg)
Parse and validate port option for CLI.
Definition parsers.c:512
bool parse_timestamp(const char *arg, void *dest, char **error_msg)
Custom parser for –seek flag.
Definition parsers.c:1010
bool parse_palette_type(const char *arg, void *dest, char **error_msg)
Parse palette type option.
Definition parsers.c:385
bool parse_verbose_flag(const char *arg, void *dest, char **error_msg)
Custom parser for –verbose flag.
Definition parsers.c:972
bool parse_audio_capture_source(const char *arg, void *dest, char **error_msg)
Definition parsers.c:1197
bool parse_color_filter(const char *arg, void *dest, char **error_msg)
Parse color filter option.
Definition parsers.c:269
bool parse_color_setting(const char *arg, void *dest, char **error_msg)
Parse color setting option (–color flag)
Definition parsers.c:160
int parse_mirror_media(const char *arg, void *config, char **remaining, int num_remaining, char **error_msg)
Parse mirror mode positional argument (file path or URL)
Definition parsers.c:879
bool parse_log_file(const char *arg, void *dest, char **error_msg)
Custom parser for log file paths (–log-file, -L)
Definition parsers.c:1127
bool parse_volume(const char *arg, void *dest, char **error_msg)
Custom parser for volume options (–volume, –speakers-volume, –microphone-volume)
Definition parsers.c:1095
bool parse_log_level(const char *arg, void *dest, char **error_msg)
Parse log level option.
Definition parsers.c:448
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:685
bool parse_render_theme(const char *arg, void *dest, char **error_msg)
Parse render theme option for pixel rendering.
Definition parsers.c:346
bool parse_color_mode(const char *arg, void *dest, char **error_msg)
Parse terminal color level option.
Definition parsers.c:210
bool parse_utf8_setting(const char *arg, void *dest, char **error_msg)
Parse UTF-8 setting option (–utf8 flag)
Definition parsers.c:191
bool parse_palette_chars(const char *arg, void *dest, char **error_msg)
Parse custom palette characters option.
Definition parsers.c:929
Custom option parsers for enum types.
📂 Path Manipulation Utilities
pcre2_code * asciichat_pcre2_singleton_get_code(pcre2_singleton_t *singleton)
Get the compiled pcre2_code from a singleton handle.
Definition pcre2.c:98
int asciichat_pcre2_match(const pcre2_code *code, PCRE2_SPTR subject, PCRE2_SIZE length, PCRE2_SIZE startoffset, uint32_t options, pcre2_match_data *match_data, pcre2_match_context *context)
Definition pcre2.c:399
pcre2_singleton_t * asciichat_pcre2_singleton_compile(const char *pattern, uint32_t flags)
Compile and cache a PCRE2 regex pattern with thread-safe singleton semantics.
Definition pcre2.c:47
🔍 Centralized PCRE2 singleton pattern for efficient regex compilation
#define SESSION_STRING_BUFFER_SIZE
Error context structure.
char * context_message
Optional custom message (dynamically allocated, owned by system)
Consolidated options structure.
palette_type_t palette_type
Selected palette type.
Represents a thread-safe compiled PCRE2 regex singleton.
Definition pcre2.c:22
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
color_filter_t
Monochromatic color filter enumeration.
Definition terminal.h:599
@ COLOR_FILTER_NONE
No filtering (default)
Definition terminal.h:601
render_mode_t
Render mode preferences.
Definition terminal.h:662
@ RENDER_MODE_FOREGROUND
Foreground colors only (text color)
Definition terminal.h:664
@ RENDER_MODE_BACKGROUND
Background colors (block colors)
Definition terminal.h:666
@ RENDER_MODE_HALF_BLOCK
Unicode half-block characters (mixed foreground/background)
Definition terminal.h:668
terminal_color_mode_t
Terminal color support levels.
Definition terminal.h:578
@ TERM_COLOR_NONE
No color support (monochrome terminal)
Definition terminal.h:582
@ TERM_COLOR_16
16-color support (standard ANSI colors)
Definition terminal.h:584
@ TERM_COLOR_256
256-color support (extended ANSI palette)
Definition terminal.h:586
@ TERM_COLOR_AUTO
Auto-detect color support from terminal capabilities.
Definition terminal.h:580
@ TERM_COLOR_TRUECOLOR
24-bit truecolor support (RGB colors)
Definition terminal.h:588
⏱️ High-precision timing utilities using sokol_time.h and uthash
#define LOG_DEV
Definition types.h:38
#define LOG_DEBUG
Definition types.h:39
#define LOG_FATAL
Definition types.h:43
#define LOG_ERROR
Definition types.h:42
#define LOG_WARN
Definition types.h:41
#define LOG_INFO
Definition types.h:40
asciichat_error_t parse_port(const char *str, uint16_t *out_port)
Parse port number (1-65535) from string.
🔍 Safe Parsing Utilities