ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
path.c File Reference

Cross-platform path manipulation with normalization and Windows/Unix separator handling. More...

Go to the source code of this file.

Functions

const char * extract_project_relative_path (const char *file)
 
char * expand_path (const char *path)
 
char * get_config_dir (void)
 
char * get_data_dir (void)
 
char * get_log_dir (void)
 
char * get_discovery_database_dir (void)
 
bool path_normalize_copy (const char *path, char *out, size_t out_len)
 
bool path_is_absolute (const char *path)
 
bool path_is_within_base (const char *path, const char *base)
 
bool path_is_within_any_base (const char *path, const char *const *bases, size_t base_count)
 
bool path_looks_like_path (const char *value)
 
asciichat_error_t path_validate_user_path (const char *input, path_role_t role, char **normalized_out)
 

Detailed Description

Cross-platform path manipulation with normalization and Windows/Unix separator handling.

Definition in file path.c.

Function Documentation

◆ expand_path()

char * expand_path ( const char *  path)

Definition at line 471 of file path.c.

471 {
472 if (path[0] == PATH_TILDE) {
473 const char *home = platform_get_home_dir();
474 if (!home) {
475 return NULL;
476 }
477
478 char *expanded;
479 size_t total_len = strlen(home) + strlen(path) + 1; // path includes the tilde
480 expanded = SAFE_MALLOC(total_len, char *);
481 if (!expanded) {
482 return NULL;
483 }
484 safe_snprintf(expanded, total_len, "%s%s", home, path + 1);
485
487
488 return expanded;
489 }
490 return platform_strdup(path);
491}
char * platform_strdup(const char *s)
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
void platform_normalize_path_separators(char *path)
Definition util.c:96
const char * platform_get_home_dir(void)
Definition util.c:92

References platform_get_home_dir(), platform_normalize_path_separators(), platform_strdup(), and safe_snprintf().

Referenced by config_create_default(), config_load_and_apply(), detect_default_ssh_key(), and path_validate_user_path().

◆ extract_project_relative_path()

const char * extract_project_relative_path ( const char *  file)

Definition at line 410 of file path.c.

410 {
411 if (!file) {
412 SET_ERRNO(ERROR_INVALID_PARAM, "file is null");
413 return "unknown";
414 }
415
416#ifdef __EMSCRIPTEN__
417 // WASM builds: Return raw path to avoid recursion in logging system
418 // extract_project_relative_path is called from format_log_header, which is part of logging.
419 // If we call normalize_path or other functions that might use SET_ERRNO or log functions,
420 // we create infinite recursion: log_msg -> format_log_header -> extract_project_relative_path -> SET_ERRNO ->
421 // log_error -> log_msg For WASM, just return the filename without path processing.
422 const char *sep = strrchr(file, '/');
423 if (!sep) {
424 sep = strrchr(file, '\\');
425 }
426 return sep ? sep + 1 : file;
427#endif
428
429 /* First normalize the path to resolve .. and . components */
430 const char *normalized = normalize_path(file);
431
432 /* Try to find and strip the project root from the absolute path */
433 char *project_root = find_project_root();
434 if (project_root) {
435 size_t root_len = strlen(project_root);
436 size_t norm_len = strlen(normalized);
437
438 /* Check if normalized path starts with project root */
439 if (norm_len > root_len && strncmp(normalized, project_root, root_len) == 0) {
440 const char *remainder = normalized + root_len;
441
442 /* Skip the separator if present */
443 if (*remainder == PATH_DELIM || *remainder == '/' || *remainder == '\\') {
444 remainder++;
445 }
446
447 free(project_root);
448 return remainder;
449 }
450 free(project_root);
451 }
452
453 /* Fallback: Extract relative path by looking for common separators */
454 const char *last_sep = strrchr(normalized, PATH_DELIM);
455 if (!last_sep) {
456 last_sep = strrchr(normalized, '/');
457 }
458 if (!last_sep) {
459 last_sep = strrchr(normalized, '\\');
460 }
461
462 /* If we found a separator, return the part after it */
463 if (last_sep) {
464 return last_sep + 1;
465 }
466
467 /* Last resort: return just the filename (don't return absolute path) */
468 return normalized;
469}

Referenced by asciichat_fatal_with_context(), asciichat_print_error_context(), log_json_async_safe(), log_json_write(), and log_template_apply().

◆ get_config_dir()

char * get_config_dir ( void  )

Definition at line 493 of file path.c.

493 {
494 /* Delegate to platform abstraction layer */
496}
char * platform_get_config_dir(void)
Definition util.c:102

References platform_get_config_dir().

Referenced by acds_main(), config_load_and_apply(), discovery_keys_get_cache_path(), get_known_hosts_path(), options_init(), and path_validate_user_path().

◆ get_data_dir()

char * get_data_dir ( void  )

Definition at line 498 of file path.c.

498 {
499 /* Delegate to platform abstraction layer */
500 return platform_get_data_dir();
501}
char * platform_get_data_dir(void)

References platform_get_data_dir().

Referenced by get_discovery_database_dir().

◆ get_discovery_database_dir()

char * get_discovery_database_dir ( void  )

Definition at line 581 of file path.c.

581 {
582#ifdef _WIN32
583 // Windows: Try %PROGRAMDATA%\ascii-chat\ first, then fall back to user directories
584 const char *program_data = platform_getenv("PROGRAMDATA");
585 if (program_data && program_data[0] != '\0') {
586 size_t len = strlen(program_data) + strlen("\\ascii-chat\\") + 1;
587 char *system_dir = SAFE_MALLOC(len, char *);
588 if (system_dir) {
589 safe_snprintf(system_dir, len, "%s\\ascii-chat\\", program_data);
590
591 // Try to create directory recursively with public permissions (755 equivalent)
592 asciichat_error_t mkdir_result = platform_mkdir_recursive(system_dir, 0755);
593 if (mkdir_result == ASCIICHAT_OK) {
594 // Directory exists or was created - check if writable
595 if (platform_access(system_dir, PLATFORM_ACCESS_WRITE) == 0) {
596 return system_dir; // System-wide location is writable
597 }
598 }
599 SAFE_FREE(system_dir);
600 }
601 }
602
603 // Fall back to user data directory
604 char *data_dir = get_data_dir();
605 if (data_dir) {
606 // Try to create the directory recursively
607 asciichat_error_t mkdir_result = platform_mkdir_recursive(data_dir, DIR_PERM_PRIVATE);
608 if (mkdir_result == ASCIICHAT_OK) {
609 if (platform_access(data_dir, PLATFORM_ACCESS_WRITE) == 0) {
610 return data_dir;
611 }
612 }
613 SAFE_FREE(data_dir);
614 }
615
616 return NULL;
617#else
618 // Unix: Try ${INSTALL_PREFIX}/var/ascii-chat/ first (system-wide, Homebrew-aware)
619 // This uses the baked-in install prefix from paths.h (e.g., /opt/homebrew or /usr/local)
620 const char *prefix = ASCIICHAT_INSTALL_PREFIX;
621 size_t system_len = strlen(prefix) + strlen("/var/ascii-chat/") + 1;
622 char *system_dir = SAFE_MALLOC(system_len, char *);
623 if (system_dir) {
624 safe_snprintf(system_dir, system_len, "%s/var/ascii-chat/", prefix);
625
626 // Try to create directory recursively with public permissions (755) for system-wide use
627 // platform_mkdir_recursive creates parent directories as needed
628 asciichat_error_t mkdir_result = platform_mkdir_recursive(system_dir, 0755);
629 if (mkdir_result == ASCIICHAT_OK) {
630 // Directory exists or was created - check if writable
631 if (platform_access(system_dir, PLATFORM_ACCESS_WRITE) == 0) {
632 return system_dir; // System-wide location is writable
633 }
634 }
635 SAFE_FREE(system_dir);
636 }
637
638#ifdef __APPLE__
639 // On macOS, /usr/local is typically user-writable (set up by Homebrew)
640 // Try it if the install prefix is different (e.g., /opt/homebrew on Apple Silicon)
641 if (strcmp(prefix, "/usr/local") != 0) {
642 const char *usr_local_path = "/usr/local/var/ascii-chat/";
643 size_t usr_local_len = strlen(usr_local_path) + 1;
644 char *usr_local_dir = SAFE_MALLOC(usr_local_len, char *);
645 if (usr_local_dir) {
646 safe_snprintf(usr_local_dir, usr_local_len, "%s", usr_local_path);
647
648 asciichat_error_t mkdir_result = platform_mkdir_recursive(usr_local_dir, 0755);
649 if (mkdir_result == ASCIICHAT_OK) {
650 if (platform_access(usr_local_dir, PLATFORM_ACCESS_WRITE) == 0) {
651 return usr_local_dir; // /usr/local is writable
652 }
653 }
654 SAFE_FREE(usr_local_dir);
655 }
656 }
657#endif
658
659 // Fall back to user data directory (XDG_DATA_HOME or ~/.local/share/ascii-chat/)
660 char *data_dir = get_data_dir();
661 if (data_dir) {
662 // Try to create the directory recursively
663 asciichat_error_t mkdir_result = platform_mkdir_recursive(data_dir, DIR_PERM_PRIVATE);
664 if (mkdir_result == ASCIICHAT_OK) {
665 if (platform_access(data_dir, PLATFORM_ACCESS_WRITE) == 0) {
666 return data_dir;
667 }
668 }
669 SAFE_FREE(data_dir);
670 }
671
672 return NULL;
673#endif
674}
char * get_data_dir(void)
Definition path.c:498
int platform_access(const char *pathname, int mode)
asciichat_error_t platform_mkdir_recursive(const char *path, int mode)
const char * platform_getenv(const char *name)
Definition wasm/system.c:13

References get_data_dir(), platform_access(), platform_getenv(), platform_mkdir_recursive(), and safe_snprintf().

Referenced by options_init().

◆ get_log_dir()

char * get_log_dir ( void  )

Definition at line 503 of file path.c.

503 {
504#ifdef __EMSCRIPTEN__
505 // WASM builds: Return NULL to skip SAFE_MALLOC before memory tracking is initialized
506 // The caller will use a fallback path (temp dir + filename)
507 return NULL;
508#endif
509
510#ifdef NDEBUG
511 // Release builds: Use $TMPDIR/ascii-chat/
512 // Get system temp directory
513 char temp_dir[PLATFORM_MAX_PATH_LENGTH];
514 if (!platform_get_temp_dir(temp_dir, sizeof(temp_dir))) {
515 // Fallback: Use current working directory if temp dir unavailable
516 char cwd_buf[PLATFORM_MAX_PATH_LENGTH];
517 if (!platform_get_cwd(cwd_buf, sizeof(cwd_buf))) {
518 return NULL;
519 }
520 char *result = SAFE_MALLOC(strlen(cwd_buf) + 1, char *);
521 if (!result) {
522 return NULL;
523 }
524 safe_snprintf(result, strlen(cwd_buf) + 1, "%s", cwd_buf);
525 return result;
526 }
527
528 // Build path to ascii-chat subdirectory
529 size_t log_dir_len = strlen(temp_dir) + strlen(PATH_SEPARATOR_STR) + strlen("ascii-chat") + 1;
530 char *log_dir = SAFE_MALLOC(log_dir_len, char *);
531 if (!log_dir) {
532 return NULL;
533 }
534 safe_snprintf(log_dir, log_dir_len, "%s%sascii-chat", temp_dir, PATH_SEPARATOR_STR);
535
536 // Create the directory if it doesn't exist (with owner-only permissions)
537 asciichat_error_t mkdir_result = platform_mkdir(log_dir, DIR_PERM_PRIVATE);
538 if (mkdir_result != ASCIICHAT_OK) {
539 // Directory creation failed - fall back to temp_dir without subdirectory
540 SAFE_FREE(log_dir);
541 char *result = SAFE_MALLOC(strlen(temp_dir) + 1, char *);
542 if (!result) {
543 return NULL;
544 }
545 safe_snprintf(result, strlen(temp_dir) + 1, "%s", temp_dir);
546 return result;
547 }
548
549 // Verify the directory is writable
550 if (platform_access(log_dir, PLATFORM_ACCESS_WRITE) != 0) {
551 // Directory not writable - fall back to temp_dir
552 SAFE_FREE(log_dir);
553 char *result = SAFE_MALLOC(strlen(temp_dir) + 1, char *);
554 if (!result) {
555 return NULL;
556 }
557 safe_snprintf(result, strlen(temp_dir) + 1, "%s", temp_dir);
558 return result;
559 }
560
561 return log_dir;
562#else
563 // Debug builds: Use repository root for logs
564 char *repo_root = find_project_root();
565 if (repo_root) {
566 return repo_root;
567 }
568
569 // Fallback to current working directory if repo root not found
570 char cwd_buf[PLATFORM_MAX_PATH_LENGTH];
571 if (!platform_get_cwd(cwd_buf, sizeof(cwd_buf))) {
572 return NULL;
573 }
574
575 char *result = SAFE_MALLOC(strlen(cwd_buf) + 1, char *);
576 safe_snprintf(result, strlen(cwd_buf) + 1, "%s", cwd_buf);
577 return result;
578#endif
579}
#define PLATFORM_MAX_PATH_LENGTH
Definition system.c:64
bool platform_get_cwd(char *cwd, size_t path_size)
Definition util.c:108
bool platform_get_temp_dir(char *temp_dir, size_t path_size)
Definition util.c:72

References platform_access(), platform_get_cwd(), platform_get_temp_dir(), PLATFORM_MAX_PATH_LENGTH, and safe_snprintf().

◆ path_is_absolute()

bool path_is_absolute ( const char *  path)

Definition at line 696 of file path.c.

696 {
697 if (!path || !*path) {
698 return false;
699 }
700
701#ifdef _WIN32
702 if ((path[0] == '\\' && path[1] == '\\')) {
703 return true; // UNC path
704 }
705 if (isalpha((unsigned char)path[0]) && path[1] == PATH_DRIVE_SEPARATOR && path[2] == PATH_DELIM) {
706 return true;
707 }
708 return false;
709#else
710 return path[0] == PATH_DELIM;
711#endif
712}

Referenced by path_is_within_base(), and path_validate_user_path().

◆ path_is_within_any_base()

bool path_is_within_any_base ( const char *  path,
const char *const *  bases,
size_t  base_count 
)

Definition at line 748 of file path.c.

748 {
749 if (!path || !bases || base_count == 0) {
750 return false;
751 }
752
753 for (size_t i = 0; i < base_count; ++i) {
754 const char *base = bases[i];
755 if (!base) {
756 continue;
757 }
758 if (path_is_within_base(path, base)) {
759 return true;
760 }
761 }
762
763 return false;
764}
bool path_is_within_base(const char *path, const char *base)
Definition path.c:714

References path_is_within_base().

Referenced by path_validate_user_path().

◆ path_is_within_base()

bool path_is_within_base ( const char *  path,
const char *  base 
)

Definition at line 714 of file path.c.

714 {
715 if (!path || !base) {
716 return false;
717 }
718
719 if (!path_is_absolute(path) || !path_is_absolute(base)) {
720 return false;
721 }
722
723 char normalized_path[PLATFORM_MAX_PATH_LENGTH];
724 char normalized_base[PLATFORM_MAX_PATH_LENGTH];
725
726 if (!path_normalize_copy(path, normalized_path, sizeof(normalized_path))) {
727 return false;
728 }
729 if (!path_normalize_copy(base, normalized_base, sizeof(normalized_base))) {
730 return false;
731 }
732
733 size_t base_len = strlen(normalized_base);
734 if (base_len == 0) {
735 return false;
736 }
737
738 if (platform_path_strcasecmp(normalized_path, normalized_base, base_len) != 0) {
739 return false;
740 }
741 char next = normalized_path[base_len];
742 if (next == '\0') {
743 return true;
744 }
745 return next == PATH_DELIM;
746}
bool path_is_absolute(const char *path)
Definition path.c:696
bool path_normalize_copy(const char *path, char *out, size_t out_len)
Definition path.c:676
int platform_path_strcasecmp(const char *a, const char *b, size_t n)
Definition util.c:117

References path_is_absolute(), path_normalize_copy(), PLATFORM_MAX_PATH_LENGTH, and platform_path_strcasecmp().

Referenced by path_is_within_any_base().

◆ path_looks_like_path()

bool path_looks_like_path ( const char *  value)

Definition at line 766 of file path.c.

766 {
767 if (!value || *value == '\0') {
768 return false;
769 }
770
771 if (value[0] == PATH_DELIM || value[0] == PATH_COMPONENT_DOT || value[0] == PATH_TILDE) {
772 return true;
773 }
774
775 if (strchr(value, PATH_DELIM)) {
776 return true;
777 }
778
779#ifdef _WIN32
780 if (isalpha((unsigned char)value[0]) && value[1] == ':' && value[2] == PATH_DELIM) {
781 return true;
782 }
783#endif
784
785 return false;
786}

Referenced by parse_keys_from_file(), parse_public_key(), parse_public_keys(), path_validate_user_path(), and validate_ssh_key_file().

◆ path_normalize_copy()

bool path_normalize_copy ( const char *  path,
char *  out,
size_t  out_len 
)

Definition at line 676 of file path.c.

676 {
677 if (!path || !out || out_len == 0) {
678 SET_ERRNO(ERROR_INVALID_PARAM, "null path or out or out_len is 0");
679 return false;
680 }
681
682 const char *normalized = normalize_path(path);
683 if (!normalized) {
684 return false;
685 }
686
687 size_t len = strlen(normalized);
688 if (len + 1 > out_len) {
689 return false;
690 }
691
692 memcpy(out, normalized, len + 1);
693 return true;
694}

Referenced by path_is_within_base(), and path_validate_user_path().

◆ path_validate_user_path()

asciichat_error_t path_validate_user_path ( const char *  input,
path_role_t  role,
char **  normalized_out 
)

Definition at line 974 of file path.c.

974 {
975 if (!normalized_out) {
976 return SET_ERRNO(map_role_to_error(role), "path_validate_user_path requires output pointer");
977 }
978 *normalized_out = NULL;
979
980 if (!input || *input == '\0') {
981 return SET_ERRNO(map_role_to_error(role), "Path is empty for role %d", role);
982 }
983
984 // SECURITY: For log files, if input is a simple filename (no separators or ..), constrain it to a safe directory
985 if (role == PATH_ROLE_LOG_FILE) {
986 // Check if input contains path separators or parent directory references
987 bool is_simple_filename = true;
988 for (const char *p = input; *p; p++) {
989 if (*p == PATH_DELIM || *p == '/' || *p == '\\') {
990 is_simple_filename = false;
991 break;
992 }
993 }
994 // Also reject ".." components (even without separators like "..something")
995 if (strstr(input, "..") != NULL) {
996 is_simple_filename = false;
997 }
998
999 // If it's a simple filename, resolve it to a safe base directory
1000 if (is_simple_filename) {
1001 // Always prefer current working directory for simple log filenames
1002 // This ensures logs go to where the user is running the command from
1003 char safe_base[PLATFORM_MAX_PATH_LENGTH];
1004
1005 if (!platform_get_cwd(safe_base, sizeof(safe_base))) {
1006 // If cwd fails, try config dir as fallback
1007 char *config_dir = get_config_dir();
1008 if (config_dir) {
1009 SAFE_STRNCPY(safe_base, config_dir, sizeof(safe_base));
1010 SAFE_FREE(config_dir);
1011 } else {
1012 return SET_ERRNO(ERROR_LOGGING_INIT, "Failed to determine safe directory for log file");
1013 }
1014 }
1015
1016 // Build the full path: safe_base + separator + input
1017 size_t base_len = strlen(safe_base);
1018 size_t input_len = strlen(input);
1019 bool needs_sep = base_len > 0 && safe_base[base_len - 1] != PATH_DELIM;
1020 size_t total_len = base_len + (needs_sep ? 1 : 0) + input_len + 1;
1021
1022 if (total_len > PLATFORM_MAX_PATH_LENGTH) {
1023 return SET_ERRNO(ERROR_LOGGING_INIT, "Log file path too long: %s/%s", safe_base, input);
1024 }
1025
1026 char resolved_buf[PLATFORM_MAX_PATH_LENGTH];
1027 safe_snprintf(resolved_buf, sizeof(resolved_buf), "%s%s%s", safe_base, needs_sep ? PATH_SEPARATOR_STR : "",
1028 input);
1029
1030 // Normalize the resolved path
1031 char normalized_buf[PLATFORM_MAX_PATH_LENGTH];
1032 if (!path_normalize_copy(resolved_buf, normalized_buf, sizeof(normalized_buf))) {
1033 return SET_ERRNO(ERROR_LOGGING_INIT, "Failed to normalize log file path: %s", resolved_buf);
1034 }
1035
1036 // Allocate and return the result
1037 char *result = SAFE_MALLOC(strlen(normalized_buf) + 1, char *);
1038 if (!result) {
1039 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate normalized path");
1040 }
1041 safe_snprintf(result, strlen(normalized_buf) + 1, "%s", normalized_buf);
1042 *normalized_out = result;
1043 return ASCIICHAT_OK;
1044 }
1045 // If not a simple filename (contains separators), continue with normal validation below
1046 }
1047
1048 // For non-log-files or log files with path separators, validate as usual
1049 if (role != PATH_ROLE_LOG_FILE && !path_looks_like_path(input)) {
1050 return SET_ERRNO(map_role_to_error(role), "Value does not look like a filesystem path: %s", input);
1051 }
1052
1053 char *expanded = expand_path(input);
1054 if (!expanded) {
1055 return SET_ERRNO(map_role_to_error(role), "Failed to expand path: %s", input);
1056 }
1057
1058 char candidate_buf[PLATFORM_MAX_PATH_LENGTH];
1059 const char *candidate_path = expanded;
1060
1061 if (!path_is_absolute(candidate_path)) {
1062 char cwd_buf[PLATFORM_MAX_PATH_LENGTH];
1063 if (!platform_get_cwd(cwd_buf, sizeof(cwd_buf))) {
1064 SAFE_FREE(expanded);
1065 return SET_ERRNO(map_role_to_error(role), "Failed to determine current working directory");
1066 }
1067
1068 size_t total_len = strlen(cwd_buf) + 1 + strlen(candidate_path) + 1;
1069 if (total_len >= sizeof(candidate_buf)) {
1070 SAFE_FREE(expanded);
1071 return SET_ERRNO(map_role_to_error(role), "Resolved path is too long: %s/%s", cwd_buf, candidate_path);
1072 }
1073 if (strlen(candidate_path) > 0 && candidate_path[0] == PATH_DELIM) {
1074 safe_snprintf(candidate_buf, sizeof(candidate_buf), "%s%s", cwd_buf, candidate_path);
1075 } else {
1076 safe_snprintf(candidate_buf, sizeof(candidate_buf), "%s%c%s", cwd_buf, PATH_DELIM, candidate_path);
1077 }
1078 candidate_path = candidate_buf;
1079 }
1080
1081 char normalized_buf[PLATFORM_MAX_PATH_LENGTH];
1082 if (!path_normalize_copy(candidate_path, normalized_buf, sizeof(normalized_buf))) {
1083 SAFE_FREE(expanded);
1084 return SET_ERRNO(map_role_to_error(role), "Failed to normalize path: %s", candidate_path);
1085 }
1086
1087 if (!path_is_absolute(normalized_buf)) {
1088 SAFE_FREE(expanded);
1089 return SET_ERRNO(map_role_to_error(role), "Normalized path is not absolute: %s", normalized_buf);
1090 }
1091
1092 const char *bases[MAX_PATH_BASES] = {0};
1093 size_t base_count = 0;
1094
1095 // Always add current working directory as an allowed base
1096 // This is critical for log files and other paths relative to where the user runs the command
1097 char cwd_base[PLATFORM_MAX_PATH_LENGTH];
1098 if (platform_get_cwd(cwd_base, sizeof(cwd_base))) {
1099 append_base_if_valid(cwd_base, bases, &base_count);
1100 }
1101
1102 char temp_base[PLATFORM_MAX_PATH_LENGTH];
1103 if (platform_get_temp_dir(temp_base, sizeof(temp_base))) {
1104 append_base_if_valid(temp_base, bases, &base_count);
1105 }
1106
1107 char *config_dir = get_config_dir();
1108 if (config_dir) {
1109 append_base_if_valid(config_dir, bases, &base_count);
1110 }
1111
1112 const char *home_env = platform_get_home_dir();
1113 if (home_env) {
1114 append_base_if_valid(home_env, bases, &base_count);
1115 }
1116
1117 char ascii_chat_home[PLATFORM_MAX_PATH_LENGTH];
1118 if (home_env) {
1119 build_ascii_chat_path(home_env, ".ascii-chat", ascii_chat_home, sizeof(ascii_chat_home));
1120 append_base_if_valid(ascii_chat_home, bases, &base_count);
1121 }
1122
1123#ifndef _WIN32
1124 char ascii_chat_home_tmp[PLATFORM_MAX_PATH_LENGTH];
1125 build_ascii_chat_path("/tmp", ".ascii-chat", ascii_chat_home_tmp, sizeof(ascii_chat_home_tmp));
1126 append_base_if_valid(ascii_chat_home_tmp, bases, &base_count);
1127#endif
1128
1129 char ssh_home[PLATFORM_MAX_PATH_LENGTH];
1130 if (home_env) {
1131 build_ascii_chat_path(home_env, ".ssh", ssh_home, sizeof(ssh_home));
1132 append_base_if_valid(ssh_home, bases, &base_count);
1133 }
1134
1135#ifdef _WIN32
1136 char program_data_logs[PLATFORM_MAX_PATH_LENGTH];
1137 const char *program_data = platform_getenv("PROGRAMDATA");
1138 if (program_data) {
1139 build_ascii_chat_path(program_data, "ascii-chat", program_data_logs, sizeof(program_data_logs));
1140 append_base_if_valid(program_data_logs, bases, &base_count);
1141 }
1142#else
1143 // System-wide config directories (for server deployments)
1144 append_base_if_valid("/etc/ascii-chat", bases, &base_count);
1145 append_base_if_valid("/usr/local/etc/ascii-chat", bases, &base_count);
1146 append_base_if_valid("/var/log", bases, &base_count);
1147 append_base_if_valid("/var/tmp", bases, &base_count);
1148 append_base_if_valid("/tmp", bases, &base_count);
1149#ifdef __APPLE__
1150 // On macOS, /tmp is a symlink to /private/tmp
1151 append_base_if_valid("/private/tmp", bases, &base_count);
1152 // On macOS, all user home directories are under /Users
1153 append_base_if_valid("/Users", bases, &base_count);
1154#endif
1155#endif
1156
1157 // Security check: Reject paths that point to sensitive system files
1158 // This applies to all path roles, not just logs
1159 if (is_sensitive_system_path(normalized_buf)) {
1160 SAFE_FREE(expanded);
1161 if (config_dir) {
1162 SAFE_FREE(config_dir);
1163 }
1164 return SET_ERRNO(map_role_to_error(role), "Cannot write to protected system path: %s", normalized_buf);
1165 }
1166
1167 // For log files, apply special validation rules
1168 if (role == PATH_ROLE_LOG_FILE) {
1169 // Check if path is a regular file (not a directory, not non-existent)
1170 // On macOS, fopen() can succeed on directories, so we must use platform_is_regular_file()
1171 bool is_regular_file = platform_is_regular_file(normalized_buf);
1172
1173 // If a regular file exists, it MUST be an ascii-chat log or empty file to be overwritten
1174 if (is_regular_file && !is_existing_ascii_chat_log(normalized_buf) && !is_file_empty(normalized_buf)) {
1175 SAFE_FREE(expanded);
1176 if (config_dir) {
1177 SAFE_FREE(config_dir);
1178 }
1179 return SET_ERRNO(ERROR_LOGGING_INIT,
1180 "Cannot overwrite existing non-ascii-chat file: %s\n"
1181 "For safety, ascii-chat will only overwrite its own log files or empty files",
1182 normalized_buf);
1183 }
1184
1185 // If file doesn't exist (or is a directory), check that path is in safe locations
1186 if (!is_regular_file) {
1187 bool allowed = base_count == 0 ? true : path_is_within_any_base(normalized_buf, bases, base_count);
1188 if (!allowed) {
1189 SAFE_FREE(expanded);
1190 if (config_dir) {
1191 SAFE_FREE(config_dir);
1192 }
1193 return SET_ERRNO(ERROR_LOGGING_INIT,
1194 "Log path %s is outside allowed directories (use -L /tmp/file.log, ~/file.log, or "
1195 "relative/absolute paths in safe locations)",
1196 normalized_buf);
1197 }
1198 }
1199 } else {
1200 // For non-log-file paths, apply standard whitelist validation
1201 bool allowed = base_count == 0 ? true : path_is_within_any_base(normalized_buf, bases, base_count);
1202 if (!allowed) {
1203 SAFE_FREE(expanded);
1204 if (config_dir) {
1205 SAFE_FREE(config_dir);
1206 }
1207 return SET_ERRNO(map_role_to_error(role), "Path %s is outside allowed directories", normalized_buf);
1208 }
1209 }
1210
1211 char *result = SAFE_MALLOC(strlen(normalized_buf) + 1, char *);
1212 if (!result) {
1213 SAFE_FREE(expanded);
1214 if (config_dir) {
1215 SAFE_FREE(config_dir);
1216 }
1217 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate normalized path");
1218 }
1219 safe_snprintf(result, strlen(normalized_buf) + 1, "%s", normalized_buf);
1220 *normalized_out = result;
1221
1222 SAFE_FREE(expanded);
1223 if (config_dir) {
1224 SAFE_FREE(config_dir);
1225 }
1226 return ASCIICHAT_OK;
1227}
bool path_looks_like_path(const char *value)
Definition path.c:766
char * expand_path(const char *path)
Definition path.c:471
char * get_config_dir(void)
Definition path.c:493
bool path_is_within_any_base(const char *path, const char *const *bases, size_t base_count)
Definition path.c:748
#define true
Definition stdbool.h:23
int platform_is_regular_file(const char *path)
Definition util.c:122

References expand_path(), get_config_dir(), path_is_absolute(), path_is_within_any_base(), path_looks_like_path(), path_normalize_copy(), platform_get_cwd(), platform_get_home_dir(), platform_get_temp_dir(), platform_getenv(), platform_is_regular_file(), PLATFORM_MAX_PATH_LENGTH, safe_snprintf(), and true.

Referenced by config_create_default(), config_load_and_apply(), parse_keys_from_file(), parse_log_file(), parse_private_key(), parse_public_key(), and validate_ssh_key_file().