ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
validation.c
Go to the documentation of this file.
1
7#include <ascii-chat/options/validation.h>
8
9#include <limits.h>
10#include <stdlib.h>
11#include <string.h>
12#include <sys/stat.h>
13
14#include <ascii-chat/common.h>
15#include <ascii-chat/log/logging.h>
16#include <ascii-chat/platform/terminal.h>
17#include <ascii-chat/platform/util.h>
18#include <ascii-chat/util/ip.h>
19#include <ascii-chat/util/parsing.h>
20#include <ascii-chat/video/palette.h>
21#include <ascii-chat/options/options.h>
22
23// ============================================================================
24// Generic Integer Range Validator
25// ============================================================================
26
41static int validate_int_range(const char *value_str, int min, int max, const char *param_name, char *error_msg,
42 size_t error_msg_size) {
43 if (!value_str || strlen(value_str) == 0) {
44 if (error_msg) {
45 SAFE_SNPRINTF(error_msg, error_msg_size, "%s value is required", param_name);
46 }
47 return INT_MIN;
48 }
49
50 int val = strtoint_safe(value_str);
51 if (val == INT_MIN || val < min || val > max) {
52 if (error_msg) {
53 if (min == max) {
54 SAFE_SNPRINTF(error_msg, error_msg_size, "Invalid %s '%s'. Must be exactly %d.", param_name, value_str, min);
55 } else if (min == 1 && max == INT_MAX) {
56 SAFE_SNPRINTF(error_msg, error_msg_size, "Invalid %s '%s'. Must be a positive integer.", param_name, value_str);
57 } else if (min == 0 && max == INT_MAX) {
58 SAFE_SNPRINTF(error_msg, error_msg_size, "Invalid %s '%s'. Must be a non-negative integer.", param_name,
59 value_str);
60 } else {
61 SAFE_SNPRINTF(error_msg, error_msg_size, "Invalid %s '%s'. Must be between %d and %d.", param_name, value_str,
62 min, max);
63 }
64 }
65 return INT_MIN;
66 }
67 return val;
68}
69
74int validate_opt_port(const char *value_str, char *error_msg, size_t error_msg_size) {
75 if (!value_str || strlen(value_str) == 0) {
76 if (error_msg) {
77 SAFE_SNPRINTF(error_msg, error_msg_size, "Port value is required");
78 }
79 return -1;
80 }
81
82 // Use safe integer parsing with range validation
83 uint16_t port_num;
84 if (parse_port(value_str, &port_num) != ASCIICHAT_OK) {
85 if (error_msg) {
86 SAFE_SNPRINTF(error_msg, error_msg_size, "Invalid port value '%s'. Port must be a number between 1 and 65535.",
87 value_str);
88 }
89 return -1;
90 }
91 return 0;
92}
93
98bool validate_port_callback(const void *options_struct, char **error_msg) {
99 const options_t *opts = (const options_t *)options_struct;
100
101 // Port is stored as an int, but we need to validate it as a string
102 // The issue is that strtol() already accepted invalid formats like " 80", "+80", "0123"
103 // So we can't validate after parsing - we need to prevent the parse in the first place
104 // This callback runs AFTER parsing, so it's too late
105
106 // Instead, check if the port value is in range (this will catch the range but not format issues)
107 if (opts->port < 1 || opts->port > 65535) {
108 if (error_msg) {
109 *error_msg = platform_strdup("Port must be between 1 and 65535");
110 }
111 return false;
112 }
113
114 return true;
115}
116
121int validate_opt_positive_int(const char *value_str, char *error_msg, size_t error_msg_size) {
122 int result = validate_int_range(value_str, 1, INT_MAX, "Value", error_msg, error_msg_size);
123 return (result == INT_MIN) ? -1 : result;
124}
125
130int validate_opt_non_negative_int(const char *value_str, char *error_msg, size_t error_msg_size) {
131 int result = validate_int_range(value_str, 0, INT_MAX, "Value", error_msg, error_msg_size);
132 return (result == INT_MIN) ? -1 : result;
133}
134
139int validate_opt_color_mode(const char *value_str, char *error_msg, size_t error_msg_size) {
140 if (!value_str) {
141 if (error_msg) {
142 SAFE_SNPRINTF(error_msg, error_msg_size, "Color mode value is required");
143 }
144 return -1;
145 }
146
147 if (strcmp(value_str, "auto") == 0) {
148 return COLOR_MODE_AUTO;
149 }
150 if (strcmp(value_str, "none") == 0 || strcmp(value_str, "mono") == 0) {
151 return COLOR_MODE_NONE;
152 }
153 if (strcmp(value_str, "16") == 0 || strcmp(value_str, "16color") == 0) {
154 return COLOR_MODE_16_COLOR;
155 }
156 if (strcmp(value_str, "256") == 0 || strcmp(value_str, "256color") == 0) {
157 return COLOR_MODE_256_COLOR;
158 }
159 if (strcmp(value_str, "truecolor") == 0 || strcmp(value_str, "24bit") == 0) {
160 return COLOR_MODE_TRUECOLOR;
161 }
162 if (error_msg) {
163 SAFE_SNPRINTF(error_msg, error_msg_size,
164 "Invalid color mode '%s'. Valid modes: auto, none, mono, 16, 256, truecolor", value_str);
165 }
166 return -1;
167}
168
173int validate_opt_render_mode(const char *value_str, char *error_msg, size_t error_msg_size) {
174 if (!value_str) {
175 if (error_msg) {
176 SAFE_SNPRINTF(error_msg, error_msg_size, "Render mode value is required");
177 }
178 return -1;
179 }
180
181 if (strcmp(value_str, "foreground") == 0 || strcmp(value_str, "fg") == 0) {
182 return RENDER_MODE_FOREGROUND;
183 }
184 if (strcmp(value_str, "background") == 0 || strcmp(value_str, "bg") == 0) {
185 return RENDER_MODE_BACKGROUND;
186 }
187 if (strcmp(value_str, "half-block") == 0 || strcmp(value_str, "halfblock") == 0) {
188 return RENDER_MODE_HALF_BLOCK;
189 }
190 if (error_msg) {
191 SAFE_SNPRINTF(error_msg, error_msg_size,
192 "Invalid render mode '%s'. Valid modes: foreground, background, half-block", value_str);
193 }
194 return -1;
195}
196
201int validate_opt_palette(const char *value_str, char *error_msg, size_t error_msg_size) {
202 if (!value_str) {
203 if (error_msg) {
204 SAFE_SNPRINTF(error_msg, error_msg_size, "Palette value is required");
205 }
206 return -1;
207 }
208
209 if (strcmp(value_str, "standard") == 0) {
210 return PALETTE_STANDARD;
211 } else if (strcmp(value_str, "blocks") == 0) {
212 return PALETTE_BLOCKS;
213 } else if (strcmp(value_str, "digital") == 0) {
214 return PALETTE_DIGITAL;
215 } else if (strcmp(value_str, "minimal") == 0) {
216 return PALETTE_MINIMAL;
217 } else if (strcmp(value_str, "cool") == 0) {
218 return PALETTE_COOL;
219 } else if (strcmp(value_str, "custom") == 0) {
220 return PALETTE_CUSTOM;
221 } else {
222 if (error_msg) {
223 SAFE_SNPRINTF(error_msg, error_msg_size,
224 "Invalid palette '%s'. Valid palettes: standard, blocks, digital, minimal, cool, custom",
225 value_str);
226 }
227 return -1;
228 }
229}
230
235int validate_opt_log_level(const char *value_str, char *error_msg, size_t error_msg_size) {
236 if (!value_str) {
237 if (error_msg) {
238 SAFE_SNPRINTF(error_msg, error_msg_size, "Log level value is required");
239 }
240 return -1;
241 }
242
243 if (platform_strcasecmp(value_str, "dev") == 0) {
244 return LOG_DEV;
245 } else if (platform_strcasecmp(value_str, "debug") == 0) {
246 return LOG_DEBUG;
247 } else if (platform_strcasecmp(value_str, "info") == 0) {
248 return LOG_INFO;
249 } else if (platform_strcasecmp(value_str, "warn") == 0) {
250 return LOG_WARN;
251 } else if (platform_strcasecmp(value_str, "error") == 0) {
252 return LOG_ERROR;
253 } else if (platform_strcasecmp(value_str, "fatal") == 0) {
254 return LOG_FATAL;
255 } else {
256 if (error_msg) {
257 SAFE_SNPRINTF(error_msg, error_msg_size,
258 "Invalid log level '%s'. Valid levels: dev, debug, info, warn, error, fatal", value_str);
259 }
260 return -1;
261 }
262}
263
269int validate_opt_ip_address(const char *value_str, char *parsed_address, size_t address_size, bool is_client,
270 char *error_msg, size_t error_msg_size) {
271 (void)is_client; // Parameter not used but kept for API consistency
272 if (!value_str || strlen(value_str) == 0) {
273 if (error_msg) {
274 SAFE_SNPRINTF(error_msg, error_msg_size, "Address value is required");
275 }
276 return -1;
277 }
278
279 // Parse IPv6 address (remove brackets if present)
280 char parsed_addr[OPTIONS_BUFF_SIZE];
281 if (parse_ipv6_address(value_str, parsed_addr, sizeof(parsed_addr)) == 0) {
282 value_str = parsed_addr;
283 }
284
285 // Check if it's a valid IPv4 address
286 if (is_valid_ipv4(value_str)) {
287 SAFE_SNPRINTF(parsed_address, address_size, "%s", value_str);
288 return 0;
289 }
290 // Check if it's a valid IPv6 address
291 if (is_valid_ipv6(value_str)) {
292 SAFE_SNPRINTF(parsed_address, address_size, "%s", value_str);
293 return 0;
294 }
295 // Check if it looks like an invalid IP (has dots but not valid IPv4 format)
296 if (strchr(value_str, '.') != NULL) {
297 if (error_msg) {
298 SAFE_SNPRINTF(error_msg, error_msg_size,
299 "Invalid IP address format '%s'. IPv4 addresses must have exactly 4 octets.", value_str);
300 }
301 return -1;
302 }
303
304 // Otherwise, try to resolve as hostname
305 char resolved_ip[OPTIONS_BUFF_SIZE];
306 if (platform_resolve_hostname_to_ipv4(value_str, resolved_ip, sizeof(resolved_ip)) == 0) {
307 SAFE_SNPRINTF(parsed_address, address_size, "%s", resolved_ip);
308 return 0;
309 } else {
310 if (error_msg) {
311 SAFE_SNPRINTF(error_msg, error_msg_size, "Failed to resolve hostname '%s' to IP address.", value_str);
312 }
313 return -1;
314 }
315}
316
321float validate_opt_float_non_negative(const char *value_str, char *error_msg, size_t error_msg_size) {
322 if (!value_str || strlen(value_str) == 0) {
323 if (error_msg) {
324 SAFE_SNPRINTF(error_msg, error_msg_size, "Value is required");
325 }
326 return -1.0f;
327 }
328
329 char *endptr;
330 float val = strtof(value_str, &endptr);
331 if (*endptr != '\0' || value_str == endptr) {
332 if (error_msg) {
333 SAFE_SNPRINTF(error_msg, error_msg_size, "Invalid float value '%s'. Must be a number.", value_str);
334 }
335 return -1.0f;
336 }
337 if (val < 0.0f) {
338 if (error_msg) {
339 SAFE_SNPRINTF(error_msg, error_msg_size, "Value must be non-negative (got %.2f)", val);
340 }
341 return -1.0f;
342 }
343 return val;
344}
345
350float validate_opt_volume(const char *value_str, char *error_msg, size_t error_msg_size) {
351 if (!value_str || strlen(value_str) == 0) {
352 if (error_msg) {
353 SAFE_SNPRINTF(error_msg, error_msg_size, "Volume value is required");
354 }
355 return -1.0f;
356 }
357
358 char *endptr;
359 float val = strtof(value_str, &endptr);
360 if (*endptr != '\0' || value_str == endptr) {
361 if (error_msg) {
362 SAFE_SNPRINTF(error_msg, error_msg_size, "Invalid volume value '%s'. Must be a number.", value_str);
363 }
364 return -1.0f;
365 }
366 if (val < 0.0f || val > 1.0f) {
367 if (error_msg) {
368 SAFE_SNPRINTF(error_msg, error_msg_size, "Volume must be between 0.0 and 1.0 (got %.2f)", val);
369 }
370 return -1.0f;
371 }
372 return val;
373}
374
379int validate_opt_max_clients(const char *value_str, char *error_msg, size_t error_msg_size) {
380 int result = validate_int_range(value_str, 1, 32, "Max clients", error_msg, error_msg_size);
381 return (result == INT_MIN) ? -1 : result;
382}
383
388int validate_opt_compression_level(const char *value_str, char *error_msg, size_t error_msg_size) {
389 int result = validate_int_range(value_str, 1, 9, "Compression level", error_msg, error_msg_size);
390 return (result == INT_MIN) ? -1 : result;
391}
392
397int validate_opt_fps(const char *value_str, char *error_msg, size_t error_msg_size) {
398 int result = validate_int_range(value_str, 1, 144, "FPS", error_msg, error_msg_size);
399 return (result == INT_MIN) ? -1 : result;
400}
401
410int validate_opt_reconnect(const char *value_str, char *error_msg, size_t error_msg_size) {
411 if (!value_str || strlen(value_str) == 0) {
412 if (error_msg) {
413 SAFE_SNPRINTF(error_msg, error_msg_size, "Reconnect value is required");
414 }
415 return INT_MIN;
416 }
417
418 // Check for string values first
419 if (platform_strcasecmp(value_str, "off") == 0) {
420 return 0; // No retries
421 }
422 if (platform_strcasecmp(value_str, "auto") == 0) {
423 return -1; // Unlimited retries
424 }
425
426 // Parse as integer
427 int val = strtoint_safe(value_str);
428 if (val == INT_MIN) {
429 if (error_msg) {
430 SAFE_SNPRINTF(error_msg, error_msg_size, "Invalid reconnect value '%s'. Use 'off', 'auto', or a number 0-999.",
431 value_str);
432 }
433 return INT_MIN;
434 }
435
436 // 0 means off, -1 means auto, 1-999 is valid range
437 if (val == 0) {
438 return 0; // No retries
439 }
440 if (val == -1) {
441 return -1; // Unlimited retries
442 }
443 if (val < 1 || val > 999) {
444 if (error_msg) {
445 SAFE_SNPRINTF(error_msg, error_msg_size, "Invalid reconnect count '%s'. Must be 'off', 'auto', or 1-999.",
446 value_str);
447 }
448 return INT_MIN;
449 }
450 return val;
451}
452
457int validate_opt_device_index(const char *value_str, char *error_msg, size_t error_msg_size) {
458 if (!value_str || strlen(value_str) == 0) {
459 if (error_msg) {
460 SAFE_SNPRINTF(error_msg, error_msg_size, "Device index value is required");
461 }
462 return INT_MIN;
463 }
464
465 int index = strtoint_safe(value_str);
466 if (index == INT_MIN) {
467 if (error_msg) {
468 SAFE_SNPRINTF(error_msg, error_msg_size,
469 "Invalid device index '%s'. Must be -1 (default) or a non-negative integer.", value_str);
470 }
471 return INT_MIN;
472 }
473
474 // -1 is valid (system default), otherwise must be >= 0
475 if (index < -1) {
476 if (error_msg) {
477 SAFE_SNPRINTF(error_msg, error_msg_size,
478 "Invalid device index '%d'. Must be -1 (default) or a non-negative integer.", index);
479 }
480 return INT_MIN;
481 }
482 return index;
483}
484
489int validate_opt_password(const char *value_str, char *error_msg, size_t error_msg_size) {
490 if (!value_str) {
491 if (error_msg) {
492 SAFE_SNPRINTF(error_msg, error_msg_size, "Password value is required");
493 }
494 return -1;
495 }
496
497 size_t len = strlen(value_str);
498 if (len < 8) {
499 if (error_msg) {
500 SAFE_SNPRINTF(error_msg, error_msg_size, "Password too short (%zu chars). Must be at least 8 characters.", len);
501 }
502 return -1;
503 }
504 if (len > 256) {
505 if (error_msg) {
506 SAFE_SNPRINTF(error_msg, error_msg_size, "Password too long (%zu chars). Must be at most 256 characters.", len);
507 }
508 return -1;
509 }
510
511 // Note: No need to check for embedded null bytes - strlen() already stopped at the first null,
512 // so by definition there are no null bytes within [0, len).
513
514 return 0;
515}
516
528int options_collect_identity_keys(options_t *opts, int argc, char *argv[]) {
529 if (!opts || !argv) {
530 log_error("options_collect_identity_keys: Invalid arguments");
531 return -1;
532 }
533
534 size_t key_count = 0;
535
536 // Scan argv for all --key or -K flags
537 for (int i = 1; i < argc && key_count < MAX_IDENTITY_KEYS; i++) {
538 const char *arg = argv[i];
539
540 // Check if this is a --key or -K flag
541 bool is_key_flag = false;
542 const char *key_value = NULL;
543
544 if (strcmp(arg, "--key") == 0 || strcmp(arg, "-K") == 0) {
545 // Next argument is the key path
546 if (i + 1 < argc) {
547 key_value = argv[i + 1];
548 is_key_flag = true;
549 i++; // Skip the value argument
550 }
551 } else if (strncmp(arg, "--key=", 6) == 0) {
552 // --key=value format
553 key_value = arg + 6;
554 is_key_flag = true;
555 }
556
557 if (is_key_flag && key_value && strlen(key_value) > 0) {
558 // Validate that local key files exist (skip for remote/virtual keys)
559 if (!is_remote_key_path(key_value)) {
560 struct stat st;
561 if (stat(key_value, &st) != 0) {
562 log_error("Key file not found: %s", key_value);
563 SET_ERRNO(ERROR_CRYPTO_KEY, "Key file not found: %s", key_value);
564 return -1;
565 }
566 // Check if it's a regular file (S_IFREG) - works on both Windows and Unix
567 if ((st.st_mode & S_IFMT) != S_IFREG) {
568 log_error("Key path is not a regular file: %s", key_value);
569 SET_ERRNO(ERROR_CRYPTO_KEY, "Key path is not a regular file: %s", key_value);
570 return -1;
571 }
572 }
573
574 // Store in identity_keys array
575 SAFE_STRNCPY(opts->identity_keys[key_count], key_value, OPTIONS_BUFF_SIZE);
576
577 // First key also goes into encrypt_key for backward compatibility
578 if (key_count == 0) {
579 SAFE_STRNCPY(opts->encrypt_key, key_value, OPTIONS_BUFF_SIZE);
580 }
581
582 key_count++;
583 log_debug("Collected identity key #%zu: %s", key_count, key_value);
584 }
585 }
586
587 opts->num_identity_keys = key_count;
588
589 if (key_count > 0) {
590 log_info("Collected %zu identity key(s) for multi-key support", key_count);
591 }
592
593 return (int)key_count;
594}
595
596bool is_remote_key_path(const char *key_path) {
597 if (!key_path || key_path[0] == '\0') {
598 return false;
599 }
600 return (strncmp(key_path, "github:", 7) == 0 ||
601 strncmp(key_path, "gitlab:", 7) == 0 ||
602 strncmp(key_path, "gpg:", 4) == 0 ||
603 strncmp(key_path, "http://", 7) == 0 ||
604 strncmp(key_path, "https://", 8) == 0);
605}
int is_valid_ipv4(const char *ip)
Definition ip.c:58
int is_valid_ipv6(const char *ip)
Definition ip.c:105
int parse_ipv6_address(const char *input, char *output, size_t output_size)
Definition ip.c:158
int strtoint_safe(const char *str)
asciichat_error_t parse_port(const char *str, uint16_t *out_port)
Definition parsing.c:251
int platform_strcasecmp(const char *s1, const char *s2)
char * platform_strdup(const char *s)
float validate_opt_volume(const char *value_str, char *error_msg, size_t error_msg_size)
Definition validation.c:350
int validate_opt_positive_int(const char *value_str, char *error_msg, size_t error_msg_size)
Definition validation.c:121
int validate_opt_render_mode(const char *value_str, char *error_msg, size_t error_msg_size)
Definition validation.c:173
int validate_opt_color_mode(const char *value_str, char *error_msg, size_t error_msg_size)
Definition validation.c:139
int validate_opt_palette(const char *value_str, char *error_msg, size_t error_msg_size)
Definition validation.c:201
int validate_opt_non_negative_int(const char *value_str, char *error_msg, size_t error_msg_size)
Definition validation.c:130
int validate_opt_password(const char *value_str, char *error_msg, size_t error_msg_size)
Definition validation.c:489
int validate_opt_fps(const char *value_str, char *error_msg, size_t error_msg_size)
Definition validation.c:397
float validate_opt_float_non_negative(const char *value_str, char *error_msg, size_t error_msg_size)
Definition validation.c:321
int validate_opt_device_index(const char *value_str, char *error_msg, size_t error_msg_size)
Definition validation.c:457
int validate_opt_reconnect(const char *value_str, char *error_msg, size_t error_msg_size)
Definition validation.c:410
bool validate_port_callback(const void *options_struct, char **error_msg)
Definition validation.c:98
int validate_opt_max_clients(const char *value_str, char *error_msg, size_t error_msg_size)
Definition validation.c:379
int validate_opt_ip_address(const char *value_str, char *parsed_address, size_t address_size, bool is_client, char *error_msg, size_t error_msg_size)
Definition validation.c:269
bool is_remote_key_path(const char *key_path)
Definition validation.c:596
int validate_opt_port(const char *value_str, char *error_msg, size_t error_msg_size)
Definition validation.c:74
int options_collect_identity_keys(options_t *opts, int argc, char *argv[])
Definition validation.c:528
int validate_opt_compression_level(const char *value_str, char *error_msg, size_t error_msg_size)
Definition validation.c:388
int validate_opt_log_level(const char *value_str, char *error_msg, size_t error_msg_size)
Definition validation.c:235