ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
path.c
Go to the documentation of this file.
1
7#include "path.h"
8#include "common.h"
9#include "platform/system.h"
10#include "platform/fs.h"
11#include <string.h>
12#include <stdbool.h>
13#include <ctype.h>
14#include <stdlib.h>
15
16/* Normalize a path by resolving .. and . components
17 * Handles both Windows (\‍) and Unix (/) separators
18 * Returns a pointer to a static buffer (not thread-safe, but sufficient for __FILE__ normalization)
19 */
20static const char *normalize_path(const char *path) {
21 if (!path) {
22 return "unknown";
23 }
24
25 static char normalized[PLATFORM_MAX_PATH_LENGTH];
26 static char components[PLATFORM_MAX_PATH_LENGTH][256];
27 int component_count = 0;
28 size_t path_len = strlen(path);
29
30 if (path_len >= PLATFORM_MAX_PATH_LENGTH) {
31 return path; /* Can't normalize, return as-is */
32 }
33
34 const char *pos = path;
35 bool absolute = false;
36
37 /* Check if path is absolute (Windows drive or Unix root) */
38#ifdef _WIN32
39 if (path_len >= 3 && isalpha((unsigned char)path[0]) && path[1] == ':' && path[2] == PATH_DELIM) {
40 absolute = true;
41 pos += 3; /* Skip the drive letter and colon and separator (e.g., "C:\") */
42 }
43#else
44 if (path_len >= 1 && path[0] == PATH_DELIM) {
45 absolute = true;
46 }
47#endif
48
49 /* Parse path into components */
50 while (*pos) {
51 /* Skip leading separators (handle both / and \ on all platforms) */
52 while (*pos == '/' || *pos == '\\') {
53 pos++;
54 }
55
56 if (!*pos)
57 break;
58
59 const char *component_start = pos;
60 while (*pos && *pos != '/' && *pos != '\\') {
61 pos++;
62 }
63
64 size_t component_len = (size_t)(pos - component_start);
65 if (component_len == 0)
66 continue;
67
68 if (component_len >= sizeof(components[0])) {
69 component_len = sizeof(components[0]) - 1;
70 }
71
72 /* Check for . and .. components */
73 if (component_len == 1 && component_start[0] == PATH_COMPONENT_DOT) {
74 /* Skip . component */
75 continue;
76 }
77 if (component_len == 2 && component_start[0] == PATH_COMPONENT_DOT && component_start[1] == PATH_COMPONENT_DOT) {
78 /* Handle .. component - go up one level */
79 if (component_count > 0) {
80 component_count--;
81 continue;
82 }
83 if (!absolute) {
84 /* For relative paths, keep .. at the start */
85 memcpy(components[component_count], component_start, component_len);
86 components[component_count][component_len] = '\0';
87 component_count++;
88 }
89 continue;
90 }
91 /* Normal component */
92 memcpy(components[component_count], component_start, component_len);
93 components[component_count][component_len] = '\0';
94 component_count++;
95 }
96
97 /* Build normalized path */
98 size_t out_pos = 0;
99#ifdef _WIN32
100 if (absolute && path_len >= 3 && isalpha((unsigned char)path[0]) && path[1] == ':') {
101 normalized[out_pos++] = path[0];
102 normalized[out_pos++] = ':';
103 normalized[out_pos++] = PATH_DELIM;
104 }
105#else
106 if (absolute) {
107 normalized[out_pos++] = PATH_DELIM;
108 }
109#endif
110
111 for (int i = 0; i < component_count; i++) {
112 if (i > 0) {
113 normalized[out_pos++] = PATH_DELIM;
114 }
115 size_t comp_len = strlen(components[i]);
116 if (out_pos + comp_len >= PLATFORM_MAX_PATH_LENGTH) {
117 break;
118 }
119 memcpy(normalized + out_pos, components[i], comp_len);
120 out_pos += comp_len;
121 }
122
123 normalized[out_pos] = '\0';
124 return normalized;
125}
126
127const char *extract_project_relative_path(const char *file) {
128 if (!file)
129 return "unknown";
130
131 /* First normalize the path to resolve .. and . components */
132 const char *normalized = normalize_path(file);
133
134 /* Extract relative path by looking for common project directories */
135 /* Look for lib/, src/, tests/, include/ in the path - make it relative from there */
136 /* This avoids embedding absolute paths in the binary */
137 /* We need to find the LAST occurrence to avoid matching parent directories */
138 /* For example: C:\Users\user\src\ascii-chat\src\client\crypto.c */
139 /* We want to match the LAST src\, not the first one */
140 const char *dirs[] = {"lib/", "src/", "tests/", "include/", "lib\\", "src\\", "tests\\", "include\\"};
141 const char *best_match = NULL;
142 size_t best_match_pos = 0;
143
144 for (size_t i = 0; i < sizeof(dirs) / sizeof(dirs[0]); i++) {
145 const char *dir = dirs[i];
146 const char *search_start = normalized;
147 const char *last_found = NULL;
148
149 /* Find the last occurrence of this directory */
150 const char *found;
151 while ((found = strstr(search_start, dir)) != NULL) {
152 last_found = found;
153 search_start = found + 1; /* Move past this match to find next one */
154 }
155
156 if (last_found) {
157 size_t pos = (size_t)(last_found - normalized);
158 /* Use the match that's closest to the end (most specific project directory) */
159 /* Higher position = further into the path = more specific */
160 if (best_match == NULL || pos > best_match_pos) {
161 best_match = last_found;
162 best_match_pos = pos;
163 }
164 }
165 }
166
167 if (best_match) {
168 /* Found a project directory - return everything from here */
169 return best_match;
170 }
171
172 /* If no common project directory found, try to find just the filename */
173 const char *last_sep = strrchr(normalized, PATH_DELIM);
174
175 if (last_sep) {
176 return last_sep + 1;
177 }
178
179 /* Last resort: return the normalized path */
180 return normalized;
181}
182
183char *expand_path(const char *path) {
184 if (path[0] == PATH_TILDE) {
185 const char *home = NULL;
186#ifdef _WIN32
187 // On Windows, try USERPROFILE first, then HOME as fallback
188 if (!(home = platform_getenv("USERPROFILE"))) {
189 if (!(home = platform_getenv("HOME"))) {
190 return NULL; // Both USERPROFILE and HOME failed
191 }
192 // HOME found, continue to expansion below
193 }
194 // USERPROFILE found, continue to expansion below
195#else
196 if (!(home = platform_getenv("HOME"))) {
197 return NULL;
198 }
199#endif
200
201 char *expanded;
202 size_t total_len = strlen(home) + strlen(path) + 1; // path includes the tilde
203 expanded = SAFE_MALLOC(total_len, char *);
204 if (!expanded) {
205 return NULL;
206 }
207 safe_snprintf(expanded, total_len, "%s%s", home, path + 1);
208
209#ifdef _WIN32
210 // Convert Unix forward slashes to Windows backslashes
211 for (char *p = expanded; *p; p++) {
212 if (*p == '/') {
213 *p = '\\';
214 }
215 }
216#endif
217
218 return expanded;
219 }
220 return platform_strdup(path);
221}
222
223char *get_config_dir(void) {
224#ifdef _WIN32
225 // Windows: Use %APPDATA%/ascii-chat/
226 const char *appdata = platform_getenv("APPDATA");
227 if (appdata && appdata[0] != '\0') {
228 size_t len = strlen(appdata) + strlen("\\ascii-chat\\") + 1;
229 char *dir = SAFE_MALLOC(len, char *);
230 if (!dir) {
231 return NULL;
232 }
233 safe_snprintf(dir, len, "%s\\ascii-chat\\", appdata);
234 return dir;
235 }
236 // Fallback to %USERPROFILE%/.ascii-chat/
237 const char *userprofile = platform_getenv("USERPROFILE");
238 if (userprofile && userprofile[0] != '\0') {
239 size_t len = strlen(userprofile) + strlen("\\.ascii-chat\\") + 1;
240 char *dir = SAFE_MALLOC(len, char *);
241 if (!dir) {
242 return NULL;
243 }
244 safe_snprintf(dir, len, "%s\\.ascii-chat\\", userprofile);
245 return dir;
246 }
247 return NULL;
248#else
249 // Unix: Use $XDG_CONFIG_HOME/ascii-chat/ if set
250 const char *xdg_config_home = platform_getenv("XDG_CONFIG_HOME");
251 if (xdg_config_home && xdg_config_home[0] != '\0') {
252 size_t len = strlen(xdg_config_home) + strlen("/ascii-chat/") + 1;
253 char *dir = SAFE_MALLOC(len, char *);
254 if (!dir) {
255 return NULL;
256 }
257 safe_snprintf(dir, len, "%s/ascii-chat/", xdg_config_home);
258 return dir;
259 }
260
261 // Fallback: ~/.ascii-chat/
262 const char *home = platform_getenv("HOME");
263 if (home && home[0] != '\0') {
264 size_t len = strlen(home) + strlen("/.ascii-chat/") + 1;
265 char *dir = SAFE_MALLOC(len, char *);
266 if (!dir) {
267 return NULL;
268 }
269 safe_snprintf(dir, len, "%s/.ascii-chat/", home);
270 return dir;
271 }
272
273 return NULL;
274#endif
275}
276
277char *get_log_dir(void) {
278#ifdef NDEBUG
279 // Release builds: Use $TMPDIR/ascii-chat/
280 // Get system temp directory
281 char temp_dir[256];
282 if (!platform_get_temp_dir(temp_dir, sizeof(temp_dir))) {
283 // Fallback: Use current working directory if temp dir unavailable
284 char cwd_buf[PLATFORM_MAX_PATH_LENGTH];
285 if (!platform_get_cwd(cwd_buf, sizeof(cwd_buf))) {
286 return NULL;
287 }
288 char *result = SAFE_MALLOC(strlen(cwd_buf) + 1, char *);
289 if (!result) {
290 return NULL;
291 }
292 safe_snprintf(result, strlen(cwd_buf) + 1, "%s", cwd_buf);
293 return result;
294 }
295
296 // Build path to ascii-chat subdirectory
297 size_t log_dir_len = strlen(temp_dir) + strlen(PATH_SEPARATOR_STR) + strlen("ascii-chat") + 1;
298 char *log_dir = SAFE_MALLOC(log_dir_len, char *);
299 if (!log_dir) {
300 return NULL;
301 }
302 safe_snprintf(log_dir, log_dir_len, "%s%sascii-chat", temp_dir, PATH_SEPARATOR_STR);
303
304 // Create the directory if it doesn't exist (with owner-only permissions)
305 asciichat_error_t mkdir_result = platform_mkdir(log_dir, DIR_PERM_PRIVATE);
306 if (mkdir_result != ASCIICHAT_OK) {
307 // Directory creation failed - fall back to temp_dir without subdirectory
308 SAFE_FREE(log_dir);
309 char *result = SAFE_MALLOC(strlen(temp_dir) + 1, char *);
310 if (!result) {
311 return NULL;
312 }
313 safe_snprintf(result, strlen(temp_dir) + 1, "%s", temp_dir);
314 return result;
315 }
316
317 // Verify the directory is writable
318 if (platform_access(log_dir, PLATFORM_ACCESS_WRITE) != 0) {
319 // Directory not writable - fall back to temp_dir
320 SAFE_FREE(log_dir);
321 char *result = SAFE_MALLOC(strlen(temp_dir) + 1, char *);
322 if (!result) {
323 return NULL;
324 }
325 safe_snprintf(result, strlen(temp_dir) + 1, "%s", temp_dir);
326 return result;
327 }
328
329 return log_dir;
330#else
331 // Debug builds: Use current working directory
332 char cwd_buf[PLATFORM_MAX_PATH_LENGTH];
333 if (!platform_get_cwd(cwd_buf, sizeof(cwd_buf))) {
334 return NULL;
335 }
336
337 char *result = SAFE_MALLOC(strlen(cwd_buf) + 1, char *);
338 if (!result) {
339 return NULL;
340 }
341 safe_snprintf(result, strlen(cwd_buf) + 1, "%s", cwd_buf);
342 return result;
343#endif
344}
345
346bool path_normalize_copy(const char *path, char *out, size_t out_len) {
347 if (!path || !out || out_len == 0) {
348 return false;
349 }
350
351 const char *normalized = normalize_path(path);
352 if (!normalized) {
353 return false;
354 }
355
356 size_t len = strlen(normalized);
357 if (len + 1 > out_len) {
358 return false;
359 }
360
361 memcpy(out, normalized, len + 1);
362 return true;
363}
364
365bool path_is_absolute(const char *path) {
366 if (!path || !*path) {
367 return false;
368 }
369
370#ifdef _WIN32
371 if ((path[0] == '\\' && path[1] == '\\')) {
372 return true; // UNC path
373 }
374 if (isalpha((unsigned char)path[0]) && path[1] == PATH_DRIVE_SEPARATOR && path[2] == PATH_DELIM) {
375 return true;
376 }
377 return false;
378#else
379 return path[0] == PATH_DELIM;
380#endif
381}
382
383bool path_is_within_base(const char *path, const char *base) {
384 if (!path || !base) {
385 return false;
386 }
387
388 if (!path_is_absolute(path) || !path_is_absolute(base)) {
389 return false;
390 }
391
392 char normalized_path[PLATFORM_MAX_PATH_LENGTH];
393 char normalized_base[PLATFORM_MAX_PATH_LENGTH];
394
395 if (!path_normalize_copy(path, normalized_path, sizeof(normalized_path))) {
396 return false;
397 }
398 if (!path_normalize_copy(base, normalized_base, sizeof(normalized_base))) {
399 return false;
400 }
401
402 size_t base_len = strlen(normalized_base);
403 if (base_len == 0) {
404 return false;
405 }
406
407#ifdef _WIN32
408 if (_strnicmp(normalized_path, normalized_base, base_len) != 0) {
409#else
410 if (strncmp(normalized_path, normalized_base, base_len) != 0) {
411#endif
412 return false;
413 }
414 char next = normalized_path[base_len];
415 if (next == '\0') {
416 return true;
417 }
418 return next == PATH_DELIM;
419}
420
421bool path_is_within_any_base(const char *path, const char *const *bases, size_t base_count) {
422 if (!path || !bases || base_count == 0) {
423 return false;
424 }
425
426 for (size_t i = 0; i < base_count; ++i) {
427 const char *base = bases[i];
428 if (!base) {
429 continue;
430 }
431 if (path_is_within_base(path, base)) {
432 return true;
433 }
434 }
435
436 return false;
437}
438
439bool path_looks_like_path(const char *value) {
440 if (!value || *value == '\0') {
441 return false;
442 }
443
444 if (value[0] == PATH_DELIM || value[0] == PATH_COMPONENT_DOT || value[0] == PATH_TILDE) {
445 return true;
446 }
447
448 if (strchr(value, PATH_DELIM)) {
449 return true;
450 }
451
452#ifdef _WIN32
453 if (isalpha((unsigned char)value[0]) && value[1] == ':' && value[2] == PATH_DELIM) {
454 return true;
455 }
456#endif
457
458 return false;
459}
460
461static asciichat_error_t map_role_to_error(path_role_t role) {
462 switch (role) {
464 return ERROR_CONFIG;
466 return ERROR_LOGGING_INIT;
470 return ERROR_CRYPTO_KEY;
471 }
472 return ERROR_GENERAL;
473}
474
475static void append_base_if_valid(const char *candidate, const char **bases, size_t *count) {
476 if (!candidate || *candidate == '\0' || *count >= MAX_PATH_BASES) {
477 return;
478 }
479 if (!path_is_absolute(candidate)) {
480 return;
481 }
482 bases[*count] = candidate;
483 (*count)++;
484}
485
486static void build_ascii_chat_path(const char *base, const char *suffix, char *out, size_t out_len) {
487 if (!base || !suffix || out_len == 0) {
488 out[0] = '\0';
489 return;
490 }
491
492 size_t base_len = strlen(base);
493 bool needs_sep = base_len > 0 && base[base_len - 1] != PATH_DELIM;
494
495 safe_snprintf(out, out_len, "%s%s%s", base, needs_sep ? PATH_SEPARATOR_STR : "", suffix);
496}
497
498asciichat_error_t path_validate_user_path(const char *input, path_role_t role, char **normalized_out) {
499 if (!normalized_out) {
500 return SET_ERRNO(map_role_to_error(role), "path_validate_user_path requires output pointer");
501 }
502 *normalized_out = NULL;
503
504 if (!input || *input == '\0') {
505 return SET_ERRNO(map_role_to_error(role), "Path is empty for role %d", role);
506 }
507
508 // SECURITY: For log files, if input is a simple filename (no separators or ..), constrain it to a safe directory
509 if (role == PATH_ROLE_LOG_FILE) {
510 // Check if input contains path separators or parent directory references
511 bool is_simple_filename = true;
512 for (const char *p = input; *p; p++) {
513 if (*p == PATH_DELIM || *p == '/' || *p == '\\') {
514 is_simple_filename = false;
515 break;
516 }
517 }
518 // Also reject ".." components (even without separators like "..something")
519 if (strstr(input, "..") != NULL) {
520 is_simple_filename = false;
521 }
522
523 // If it's a simple filename, resolve it to a safe base directory
524 if (is_simple_filename) {
525 // Use config dir if available, otherwise current directory
526 char *config_dir = get_config_dir();
527 char safe_base[PLATFORM_MAX_PATH_LENGTH];
528
529 if (config_dir) {
530 SAFE_STRNCPY(safe_base, config_dir, sizeof(safe_base));
531 SAFE_FREE(config_dir);
532 } else {
533 // Fallback to current working directory
534 if (!platform_get_cwd(safe_base, sizeof(safe_base))) {
535 return SET_ERRNO(ERROR_LOGGING_INIT, "Failed to determine safe directory for log file");
536 }
537 }
538
539 // Build the full path: safe_base + separator + input
540 size_t base_len = strlen(safe_base);
541 size_t input_len = strlen(input);
542 bool needs_sep = base_len > 0 && safe_base[base_len - 1] != PATH_DELIM;
543 size_t total_len = base_len + (needs_sep ? 1 : 0) + input_len + 1;
544
545 if (total_len > PLATFORM_MAX_PATH_LENGTH) {
546 return SET_ERRNO(ERROR_LOGGING_INIT, "Log file path too long: %s/%s", safe_base, input);
547 }
548
549 char resolved_buf[PLATFORM_MAX_PATH_LENGTH];
550 safe_snprintf(resolved_buf, sizeof(resolved_buf), "%s%s%s", safe_base, needs_sep ? PATH_SEPARATOR_STR : "",
551 input);
552
553 // Normalize the resolved path
554 char normalized_buf[PLATFORM_MAX_PATH_LENGTH];
555 if (!path_normalize_copy(resolved_buf, normalized_buf, sizeof(normalized_buf))) {
556 return SET_ERRNO(ERROR_LOGGING_INIT, "Failed to normalize log file path: %s", resolved_buf);
557 }
558
559 // Allocate and return the result
560 char *result = SAFE_MALLOC(strlen(normalized_buf) + 1, char *);
561 if (!result) {
562 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate normalized path");
563 }
564 safe_snprintf(result, strlen(normalized_buf) + 1, "%s", normalized_buf);
565 *normalized_out = result;
566 return ASCIICHAT_OK;
567 }
568 // If not a simple filename (contains separators), continue with normal validation below
569 }
570
571 // For non-log-files or log files with path separators, validate as usual
572 if (role != PATH_ROLE_LOG_FILE && !path_looks_like_path(input)) {
573 return SET_ERRNO(map_role_to_error(role), "Value does not look like a filesystem path: %s", input);
574 }
575
576 char *expanded = expand_path(input);
577 if (!expanded) {
578 return SET_ERRNO(map_role_to_error(role), "Failed to expand path: %s", input);
579 }
580
581 char candidate_buf[PLATFORM_MAX_PATH_LENGTH];
582 const char *candidate_path = expanded;
583
584 if (!path_is_absolute(candidate_path)) {
585 char cwd_buf[PLATFORM_MAX_PATH_LENGTH];
586 if (!platform_get_cwd(cwd_buf, sizeof(cwd_buf))) {
587 SAFE_FREE(expanded);
588 return SET_ERRNO(map_role_to_error(role), "Failed to determine current working directory");
589 }
590
591 size_t total_len = strlen(cwd_buf) + 1 + strlen(candidate_path) + 1;
592 if (total_len >= sizeof(candidate_buf)) {
593 SAFE_FREE(expanded);
594 return SET_ERRNO(map_role_to_error(role), "Resolved path is too long: %s/%s", cwd_buf, candidate_path);
595 }
596 if (strlen(candidate_path) > 0 && candidate_path[0] == PATH_DELIM) {
597 safe_snprintf(candidate_buf, sizeof(candidate_buf), "%s%s", cwd_buf, candidate_path);
598 } else {
599 safe_snprintf(candidate_buf, sizeof(candidate_buf), "%s%c%s", cwd_buf, PATH_DELIM, candidate_path);
600 }
601 candidate_path = candidate_buf;
602 }
603
604 char normalized_buf[PLATFORM_MAX_PATH_LENGTH];
605 if (!path_normalize_copy(candidate_path, normalized_buf, sizeof(normalized_buf))) {
606 SAFE_FREE(expanded);
607 return SET_ERRNO(map_role_to_error(role), "Failed to normalize path: %s", candidate_path);
608 }
609
610 if (!path_is_absolute(normalized_buf)) {
611 SAFE_FREE(expanded);
612 return SET_ERRNO(map_role_to_error(role), "Normalized path is not absolute: %s", normalized_buf);
613 }
614
615 const char *bases[MAX_PATH_BASES] = {0};
616 size_t base_count = 0;
617
618 char cwd_base[PLATFORM_MAX_PATH_LENGTH];
619 if (platform_get_cwd(cwd_base, sizeof(cwd_base))) {
620 append_base_if_valid(cwd_base, bases, &base_count);
621 }
622
623 char temp_base[PLATFORM_MAX_PATH_LENGTH];
624 if (platform_get_temp_dir(temp_base, sizeof(temp_base))) {
625 append_base_if_valid(temp_base, bases, &base_count);
626 }
627
628 char *config_dir = get_config_dir();
629 if (config_dir) {
630 append_base_if_valid(config_dir, bases, &base_count);
631 }
632
633 const char *home_env = platform_getenv("HOME");
634#ifdef _WIN32
635 if (!home_env) {
636 home_env = platform_getenv("USERPROFILE");
637 }
638#endif
639 if (home_env) {
640 append_base_if_valid(home_env, bases, &base_count);
641 }
642
643 char ascii_chat_home[PLATFORM_MAX_PATH_LENGTH];
644 if (home_env) {
645 build_ascii_chat_path(home_env, ".ascii-chat", ascii_chat_home, sizeof(ascii_chat_home));
646 append_base_if_valid(ascii_chat_home, bases, &base_count);
647 }
648
649#ifndef _WIN32
650 char ascii_chat_home_tmp[PLATFORM_MAX_PATH_LENGTH];
651 build_ascii_chat_path("/tmp", ".ascii-chat", ascii_chat_home_tmp, sizeof(ascii_chat_home_tmp));
652 append_base_if_valid(ascii_chat_home_tmp, bases, &base_count);
653#endif
654
655 char ssh_home[PLATFORM_MAX_PATH_LENGTH];
656 if (home_env) {
657 build_ascii_chat_path(home_env, ".ssh", ssh_home, sizeof(ssh_home));
658 append_base_if_valid(ssh_home, bases, &base_count);
659 }
660
661#ifdef _WIN32
662 char program_data_logs[PLATFORM_MAX_PATH_LENGTH];
663 const char *program_data = platform_getenv("PROGRAMDATA");
664 if (program_data) {
665 build_ascii_chat_path(program_data, "ascii-chat", program_data_logs, sizeof(program_data_logs));
666 append_base_if_valid(program_data_logs, bases, &base_count);
667 }
668#else
669 // System-wide config directories (for server deployments)
670 append_base_if_valid("/etc/ascii-chat", bases, &base_count);
671 append_base_if_valid("/usr/local/etc/ascii-chat", bases, &base_count);
672 append_base_if_valid("/var/log", bases, &base_count);
673 append_base_if_valid("/var/tmp", bases, &base_count);
674#endif
675
676 // Validate that the path is within allowed directories
677 // Note: Simple log filenames without separators are already resolved to safe dirs above
678 bool allowed = base_count == 0 ? true : path_is_within_any_base(normalized_buf, bases, base_count);
679
680 if (!allowed) {
681 SAFE_FREE(expanded);
682 if (config_dir) {
683 SAFE_FREE(config_dir);
684 }
685 return SET_ERRNO(map_role_to_error(role), "Path %s is outside allowed directories", normalized_buf);
686 }
687
688 char *result = SAFE_MALLOC(strlen(normalized_buf) + 1, char *);
689 if (!result) {
690 SAFE_FREE(expanded);
691 if (config_dir) {
692 SAFE_FREE(config_dir);
693 }
694 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate normalized path");
695 }
696 safe_snprintf(result, strlen(normalized_buf) + 1, "%s", normalized_buf);
697 *normalized_out = result;
698
699 SAFE_FREE(expanded);
700 if (config_dir) {
701 SAFE_FREE(config_dir);
702 }
703 return ASCIICHAT_OK;
704}
Cross-platform file system operations.
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:358
#define SAFE_FREE(ptr)
Definition common.h:320
#define SAFE_MALLOC(size, cast)
Definition common.h:208
#define PLATFORM_MAX_PATH_LENGTH
Definition common.h:91
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:46
@ ERROR_LOGGING_INIT
Definition error_codes.h:56
@ ERROR_CRYPTO_KEY
Definition error_codes.h:89
@ ERROR_MEMORY
Definition error_codes.h:53
@ ASCIICHAT_OK
Definition error_codes.h:48
@ ERROR_GENERAL
Definition error_codes.h:49
@ ERROR_CONFIG
Definition error_codes.h:54
#define DIR_PERM_PRIVATE
Directory permission: Private (owner read/write/execute only)
Definition system.h:649
asciichat_error_t platform_mkdir(const char *path, int mode)
Create a directory.
bool platform_get_cwd(char *cwd, size_t path_size)
Get the current working directory of the process.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe version of snprintf that ensures null termination.
#define PATH_SEPARATOR_STR
Definition system.h:606
int platform_access(const char *path, int mode)
Check file/directory access permissions.
#define PATH_DELIM
Platform-specific path separator character.
Definition system.h:605
const char * platform_getenv(const char *name)
Get an environment variable value.
char * platform_strdup(const char *s)
Duplicate string (strdup replacement)
#define PLATFORM_ACCESS_WRITE
Check if file/directory is writable.
Definition system.h:787
bool platform_get_temp_dir(char *temp_dir, size_t path_size)
Get the system temporary directory path.
#define PATH_DRIVE_SEPARATOR
Path component: Windows drive separator (colon)
Definition path.h:78
bool path_looks_like_path(const char *value)
Determine if a string is likely intended to reference the filesystem.
Definition path.c:439
char * get_log_dir(void)
Get log directory path appropriate for current build type.
Definition path.c:277
path_role_t
Classification for user-supplied filesystem paths.
Definition path.h:253
#define MAX_PATH_BASES
Maximum number of path base directories.
Definition path.h:85
bool path_is_absolute(const char *path)
Determine whether a path is absolute on the current platform.
Definition path.c:365
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:498
char * expand_path(const char *path)
Expand path with tilde (~) support.
Definition path.c:183
bool path_is_within_base(const char *path, const char *base)
Check whether a path resides within a specified base directory.
Definition path.c:383
char * get_config_dir(void)
Get configuration directory path with XDG_CONFIG_HOME support.
Definition path.c:223
bool path_is_within_any_base(const char *path, const char *const *bases, size_t base_count)
Check whether a path resides within any of several base directories.
Definition path.c:421
#define PATH_COMPONENT_DOT
Path component: current directory (single dot)
Definition path.h:59
const char * extract_project_relative_path(const char *file)
Extract relative path from an absolute path.
Definition path.c:127
#define PATH_TILDE
Path component: home directory tilde.
Definition path.h:73
bool path_normalize_copy(const char *path, char *out, size_t out_len)
Normalize a path and copy it into the provided buffer.
Definition path.c:346
@ PATH_ROLE_CONFIG_FILE
Definition path.h:254
@ PATH_ROLE_CLIENT_KEYS
Definition path.h:258
@ PATH_ROLE_KEY_PUBLIC
Definition path.h:257
@ PATH_ROLE_LOG_FILE
Definition path.h:255
@ PATH_ROLE_KEY_PRIVATE
Definition path.h:256
📂 Path Manipulation Utilities
#define true
Definition stdbool.h:23
Cross-platform system functions interface for ascii-chat.
🔤 String Manipulation and Shell Escaping Utilities