ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
path.c
Go to the documentation of this file.
1
9#include <ascii-chat/common.h>
14#include <ascii-chat/paths.h>
15#include <string.h>
16#include <stdbool.h>
17#include <ctype.h>
18#include <stdlib.h>
19#include <stdio.h>
20#include <sys/stat.h>
22#include <pcre2.h>
23
31static const char *PATH_SEPARATOR_PATTERN = "[/\\\\]+";
32static const char *PATH_DOT_COMPONENT_PATTERN = "^\\.\\.*$";
33
34static pcre2_singleton_t *g_path_separator_regex = NULL;
35static pcre2_singleton_t *g_path_dot_component_regex = NULL;
36
41static pcre2_code *path_separator_regex_get(void) {
42 if (g_path_separator_regex == NULL) {
43 g_path_separator_regex = asciichat_pcre2_singleton_compile(PATH_SEPARATOR_PATTERN, 0);
44 }
45 return asciichat_pcre2_singleton_get_code(g_path_separator_regex);
46}
47
52static pcre2_code *path_dot_component_regex_get(void) {
53 if (g_path_dot_component_regex == NULL) {
54 g_path_dot_component_regex = asciichat_pcre2_singleton_compile(PATH_DOT_COMPONENT_PATTERN, 0);
55 }
56 return asciichat_pcre2_singleton_get_code(g_path_dot_component_regex);
57}
58
63static inline pcre2_match_data *path_match_data_create(pcre2_code *regex) {
64 if (!regex)
65 return NULL;
66 return pcre2_match_data_create_from_pattern(regex, NULL);
67}
68
72static inline void path_match_data_free(pcre2_match_data *match_data) {
73 if (match_data) {
74 pcre2_match_data_free(match_data);
75 }
76}
77
86 /* No-op: match data is now allocated and freed immediately per operation */
87}
88
89/* Normalize a path by resolving .. and . components
90 * Handles both Windows (\‍) and Unix (/) separators
91 * Returns a pointer to a static buffer (not thread-safe, but sufficient for __FILE__ normalization)
92 */
93static const char *normalize_path(const char *path) {
94 if (!path) {
95 SET_ERRNO(ERROR_INVALID_PARAM, "path is null");
96 return "unknown";
97 }
98
99 /* Use thread-local storage to avoid buffer corruption when multiple threads
100 * or rapid calls reuse the same static buffer. Each thread gets its own buffer. */
101 static _Thread_local char normalized[PLATFORM_MAX_PATH_LENGTH];
102 static _Thread_local char components[PLATFORM_MAX_PATH_LENGTH][256];
103 int component_count = 0;
104 size_t path_len = strlen(path);
105
106 if (path_len >= PLATFORM_MAX_PATH_LENGTH) {
107 return path; /* Can't normalize, return as-is */
108 }
109
110 const char *pos = path;
111 bool absolute = path_is_absolute(path);
112
113 /* Skip past the absolute path prefix if present */
114 if (absolute) {
115#ifdef _WIN32
116 if (path_len >= 3 && isalpha((unsigned char)path[0]) && path[1] == ':' && path[2] == PATH_DELIM) {
117 pos += 3; /* Skip the drive letter and colon and separator (e.g., "C:\") */
118 }
119#else
120 if (path_len >= 1 && path[0] == PATH_DELIM) {
121 pos += 1; /* Skip the root separator */
122 }
123#endif
124 }
125
126 /* Use PCRE2 to split by separators and detect dot components.
127 Allocate fresh match data for this operation and free immediately when done. */
128 pcre2_code *separator_regex = path_separator_regex_get();
129 pcre2_code *dot_regex = path_dot_component_regex_get();
130
131 if (separator_regex && dot_regex) {
132 pcre2_match_data *sep_match = path_match_data_create(separator_regex);
133 pcre2_match_data *dot_match = path_match_data_create(dot_regex);
134
135 if (sep_match && dot_match) {
136 size_t offset = 0;
137 size_t remaining_len = strlen(pos);
138 size_t last_component_end = 0;
139
140 while (offset <= remaining_len) {
141 /* Find next separator */
142 int rc = asciichat_pcre2_match(separator_regex, (PCRE2_SPTR8)pos, remaining_len, offset, 0, sep_match, NULL);
143 PCRE2_SIZE *ovector = pcre2_get_ovector_pointer(sep_match);
144 PCRE2_SIZE sep_start, sep_end;
145
146 if (rc >= 0) {
147 sep_start = ovector[0];
148 sep_end = ovector[1];
149 } else {
150 /* No more separators - handle last component if any */
151 sep_start = remaining_len;
152 sep_end = remaining_len;
153 }
154
155 /* Extract component between last_component_end and sep_start */
156 size_t component_len = sep_start - last_component_end;
157 if (component_len > 0) {
158 if (component_len >= sizeof(components[0])) {
159 component_len = sizeof(components[0]) - 1;
160 }
161
162 char temp_component[256];
163 memcpy(temp_component, pos + last_component_end, component_len);
164 temp_component[component_len] = '\0';
165
166 /* Check if component matches dot pattern (^\.\.*$) */
167 int is_dot =
168 asciichat_pcre2_match(dot_regex, (PCRE2_SPTR8)temp_component, component_len, 0, 0, dot_match, NULL);
169
170 if (is_dot >= 0) {
171 /* It's ".", "..", or "..." */
172 if (component_len == 1) {
173 /* Skip "." */
174 } else if (component_len == 2) {
175 /* Handle ".." */
176 if (component_count > 0) {
177 component_count--;
178 } else if (!absolute) {
179 memcpy(components[component_count], temp_component, component_len);
180 components[component_count][component_len] = '\0';
181 component_count++;
182 }
183 } else {
184 /* "..." or more - treat as normal component */
185 memcpy(components[component_count], temp_component, component_len);
186 components[component_count][component_len] = '\0';
187 component_count++;
188 }
189 } else {
190 /* Normal component */
191 memcpy(components[component_count], temp_component, component_len);
192 components[component_count][component_len] = '\0';
193 component_count++;
194 }
195 }
196
197 if (rc < 0) {
198 break; /* No more separators */
199 }
200
201 offset = sep_end;
202 last_component_end = sep_end;
203 }
204
205 /* Free match data immediately - it was allocated for this operation only */
206 path_match_data_free(sep_match);
207 path_match_data_free(dot_match);
208 } else {
209 /* Fallback to manual parsing if match data allocation failed */
210 const char *parse_pos = pos;
211 while (*parse_pos) {
212 while (*parse_pos == '/' || *parse_pos == '\\') {
213 parse_pos++;
214 }
215 if (!*parse_pos)
216 break;
217 const char *component_start = parse_pos;
218 while (*parse_pos && *parse_pos != '/' && *parse_pos != '\\') {
219 parse_pos++;
220 }
221 size_t component_len = (size_t)(parse_pos - component_start);
222 if (component_len == 0)
223 continue;
224 if (component_len >= sizeof(components[0])) {
225 component_len = sizeof(components[0]) - 1;
226 }
227 if (component_len == 1 && component_start[0] == PATH_COMPONENT_DOT) {
228 continue;
229 }
230 if (component_len == 2 && component_start[0] == PATH_COMPONENT_DOT &&
231 component_start[1] == PATH_COMPONENT_DOT) {
232 if (component_count > 0) {
233 component_count--;
234 continue;
235 }
236 if (!absolute) {
237 memcpy(components[component_count], component_start, component_len);
238 components[component_count][component_len] = '\0';
239 component_count++;
240 }
241 continue;
242 }
243 memcpy(components[component_count], component_start, component_len);
244 components[component_count][component_len] = '\0';
245 component_count++;
246 }
247 }
248 } else {
249 /* Fallback to manual parsing if regex compilation failed */
250 const char *parse_pos = pos;
251 while (*parse_pos) {
252 while (*parse_pos == '/' || *parse_pos == '\\') {
253 parse_pos++;
254 }
255 if (!*parse_pos)
256 break;
257 const char *component_start = parse_pos;
258 while (*parse_pos && *parse_pos != '/' && *parse_pos != '\\') {
259 parse_pos++;
260 }
261 size_t component_len = (size_t)(parse_pos - component_start);
262 if (component_len == 0)
263 continue;
264 if (component_len >= sizeof(components[0])) {
265 component_len = sizeof(components[0]) - 1;
266 }
267 if (component_len == 1 && component_start[0] == PATH_COMPONENT_DOT) {
268 continue;
269 }
270 if (component_len == 2 && component_start[0] == PATH_COMPONENT_DOT && component_start[1] == PATH_COMPONENT_DOT) {
271 if (component_count > 0) {
272 component_count--;
273 continue;
274 }
275 if (!absolute) {
276 memcpy(components[component_count], component_start, component_len);
277 components[component_count][component_len] = '\0';
278 component_count++;
279 }
280 continue;
281 }
282 memcpy(components[component_count], component_start, component_len);
283 components[component_count][component_len] = '\0';
284 component_count++;
285 }
286 }
287
288 /* Build normalized path */
289 size_t out_pos = 0;
290#ifdef _WIN32
291 if (absolute && path_len >= 3 && isalpha((unsigned char)path[0]) && path[1] == ':') {
292 normalized[out_pos++] = path[0];
293 normalized[out_pos++] = ':';
294 normalized[out_pos++] = PATH_DELIM;
295 }
296#else
297 if (absolute) {
298 normalized[out_pos++] = PATH_DELIM;
299 }
300#endif
301
302 for (int i = 0; i < component_count; i++) {
303 if (i > 0) {
304 normalized[out_pos++] = PATH_DELIM;
305 }
306 size_t comp_len = strlen(components[i]);
307 if (out_pos + comp_len >= PLATFORM_MAX_PATH_LENGTH) {
308 break;
309 }
310 memcpy(normalized + out_pos, components[i], comp_len);
311 out_pos += comp_len;
312 }
313
314 normalized[out_pos] = '\0';
315 return normalized;
316}
317
333static char *find_project_root(void) {
334 // Cache the result so we don't repeatedly call stat() which can hang during shutdown
335 static char cached_root[PLATFORM_MAX_PATH_LENGTH] = {0};
336 static bool search_done = false;
337
338 if (search_done) {
339 if (cached_root[0] != '\0') {
340 // Return a new allocation using malloc (not SAFE_MALLOC) to avoid debug_malloc
341 // recursion when called during memory report while holding g_mem.mutex
342 size_t root_len = strlen(cached_root) + 1;
343 char *copy = malloc(root_len);
344 if (copy) {
345 asciichat_error_t strcpy_result = SAFE_STRCPY(copy, root_len, cached_root);
346 if (strcpy_result != ASCIICHAT_OK) {
347 log_error("Failed to copy project root: %s", asciichat_error_string(strcpy_result));
348 free(copy);
349 return NULL;
350 }
351 }
352 return copy;
353 }
354 return NULL; // Not found
355 }
356
357 char cwd_buf[PLATFORM_MAX_PATH_LENGTH];
358 if (!platform_get_cwd(cwd_buf, sizeof(cwd_buf))) {
359 search_done = true;
360 return NULL;
361 }
362
363 char search_path[PLATFORM_MAX_PATH_LENGTH];
364 SAFE_STRNCPY(search_path, cwd_buf, sizeof(search_path));
365
366 // Search up to 128 levels deep for repository root
367 for (int i = 0; i < 128; i++) {
368 // Check for .git directory
369 char git_check[PLATFORM_MAX_PATH_LENGTH];
370 safe_snprintf(git_check, sizeof(git_check), "%s%s.git", search_path, PATH_SEPARATOR_STR);
371 struct stat git_stat;
372 bool git_found = stat(git_check, &git_stat) == 0 && S_ISDIR(git_stat.st_mode);
373 if (git_found) {
374 // Found .git directory - this is the repo root
375 safe_snprintf(cached_root, sizeof(cached_root), "%s", search_path);
376 search_done = true;
377 // Return a new allocation using malloc (not SAFE_MALLOC) to avoid debug_malloc
378 // recursion when called during memory report while holding g_mem.mutex
379 size_t path_len = strlen(search_path) + 1;
380 char *result = malloc(path_len);
381 if (result) {
382 asciichat_error_t strcpy_result = SAFE_STRCPY(result, path_len, search_path);
383 if (strcpy_result != ASCIICHAT_OK) {
384 log_error("Failed to copy search path (git root): %s", asciichat_error_string(strcpy_result));
385 free(result);
386 return NULL;
387 }
388 }
389 return result;
390 }
391
392 // We might be built from a tarball and not a git repo. But I still want this to work.
393 // So check for LICENSE.txt and CMakeLists.txt (ascii-chat's license is in the repo root with the main
394 // CMakeLists.txt).
395 char license_check[PLATFORM_MAX_PATH_LENGTH];
396 safe_snprintf(license_check, sizeof(license_check), "%s%sLICENSE.txt", search_path, PATH_SEPARATOR_STR);
397 char cmake_check[PLATFORM_MAX_PATH_LENGTH];
398 safe_snprintf(cmake_check, sizeof(cmake_check), "%s%sCMakeLists.txt", search_path, PATH_SEPARATOR_STR);
399 struct stat license_stat;
400 bool license_found = stat(license_check, &license_stat) == 0 && S_ISREG(license_stat.st_mode);
401 struct stat cmake_stat;
402 bool cmake_found = stat(cmake_check, &cmake_stat) == 0 && S_ISREG(cmake_stat.st_mode);
403 if (license_found && cmake_found) {
404 // Found LICENSE.txt
405 safe_snprintf(cached_root, sizeof(cached_root), "%s", search_path);
406 search_done = true;
407 // Return a new allocation using malloc (not SAFE_MALLOC) to avoid debug_malloc
408 // recursion when called during memory report while holding g_mem.mutex
409 size_t path_len = strlen(search_path) + 1;
410 char *result = malloc(path_len);
411 if (result) {
412 asciichat_error_t strcpy_result = SAFE_STRCPY(result, path_len, search_path);
413 if (strcpy_result != ASCIICHAT_OK) {
414 log_error("Failed to copy search path (license root): %s", asciichat_error_string(strcpy_result));
415 free(result);
416 return NULL;
417 }
418 }
419 return result;
420 }
421
422 // Go up one directory
423 size_t len = strlen(search_path);
424 if (len == 0 || (len == 1 && search_path[0] == '/')) {
425 // Hit root directory without finding .git or CMakeLists.txt
426 break;
427 }
428
429 // Remove trailing separator if present
430 if (search_path[len - 1] == '/' || search_path[len - 1] == '\\') {
431 search_path[len - 1] = '\0';
432 len--;
433 }
434
435 // Find last separator
436 int last_sep = -1;
437 for (int j = (int)len - 1; j >= 0; j--) {
438 if (search_path[j] == '/' || search_path[j] == '\\') {
439 last_sep = j;
440 break;
441 }
442 }
443
444 if (last_sep <= 0) {
445 // Can't go up further
446 break;
447 }
448
449 search_path[last_sep] = '\0';
450 }
451
452 search_done = true;
453 return NULL;
454}
455
456const char *extract_project_relative_path(const char *file) {
457 if (!file) {
458 SET_ERRNO(ERROR_INVALID_PARAM, "file is null");
459 return "unknown";
460 }
461
462#ifdef __EMSCRIPTEN__
463 // WASM builds: Return raw path to avoid recursion in logging system
464 // extract_project_relative_path is called from format_log_header, which is part of logging.
465 // If we call normalize_path or other functions that might use SET_ERRNO or log functions,
466 // we create infinite recursion: log_msg -> format_log_header -> extract_project_relative_path -> SET_ERRNO ->
467 // log_error -> log_msg For WASM, just return the filename without path processing.
468 const char *sep = strrchr(file, '/');
469 if (!sep) {
470 sep = strrchr(file, '\\');
471 }
472 return sep ? sep + 1 : file;
473#endif
474
475 /* First normalize the path to resolve .. and . components */
476 const char *normalized = normalize_path(file);
477
478 /* Try to find and strip the project root from the absolute path */
479 char *project_root = find_project_root();
480 if (project_root) {
481 size_t root_len = strlen(project_root);
482 size_t norm_len = strlen(normalized);
483
484 /* Check if normalized path starts with project root */
485 if (norm_len > root_len && strncmp(normalized, project_root, root_len) == 0) {
486 const char *remainder = normalized + root_len;
487
488 /* Skip the separator if present */
489 if (*remainder == PATH_DELIM || *remainder == '/' || *remainder == '\\') {
490 remainder++;
491 }
492
493 free(project_root);
494 return remainder;
495 }
496 free(project_root);
497 }
498
499 /* Fallback: Project root detection failed, but we should still try to return
500 * a meaningful relative path, not just the filename. Return the normalized path
501 * which should contain the full directory structure. */
502 return normalized;
503}
504
505char *expand_path(const char *path) {
506 if (path[0] == PATH_TILDE) {
507 const char *home = platform_get_home_dir();
508 if (!home) {
509 return NULL;
510 }
511
512 char *expanded;
513 size_t total_len = strlen(home) + strlen(path) + 1; // path includes the tilde
514 expanded = SAFE_MALLOC(total_len, char *);
515 if (!expanded) {
516 return NULL;
517 }
518 safe_snprintf(expanded, total_len, "%s%s", home, path + 1);
519
521
522 return expanded;
523 }
524 return platform_strdup(path);
525}
526
527char *get_config_dir(void) {
528 /* Delegate to platform abstraction layer */
530}
531
532char *get_data_dir(void) {
533 /* Delegate to platform abstraction layer */
534 return platform_get_data_dir();
535}
536
537char *get_log_dir(void) {
538#ifdef __EMSCRIPTEN__
539 // WASM builds: Return NULL to skip SAFE_MALLOC before memory tracking is initialized
540 // The caller will use a fallback path (temp dir + filename)
541 return NULL;
542#endif
543
544#ifdef NDEBUG
545 // Release builds: Use $TMPDIR/ascii-chat/
546 // Get system temp directory
547 char temp_dir[PLATFORM_MAX_PATH_LENGTH];
548 if (!platform_get_temp_dir(temp_dir, sizeof(temp_dir))) {
549 // Fallback: Use current working directory if temp dir unavailable
550 char cwd_buf[PLATFORM_MAX_PATH_LENGTH];
551 if (!platform_get_cwd(cwd_buf, sizeof(cwd_buf))) {
552 return NULL;
553 }
554 char *result = SAFE_MALLOC(strlen(cwd_buf) + 1, char *);
555 if (!result) {
556 return NULL;
557 }
558 safe_snprintf(result, strlen(cwd_buf) + 1, "%s", cwd_buf);
559 return result;
560 }
561
562 // Build path to ascii-chat subdirectory
563 size_t log_dir_len = strlen(temp_dir) + strlen(PATH_SEPARATOR_STR) + strlen("ascii-chat") + 1;
564 char *log_dir = SAFE_MALLOC(log_dir_len, char *);
565 if (!log_dir) {
566 return NULL;
567 }
568 safe_snprintf(log_dir, log_dir_len, "%s%sascii-chat", temp_dir, PATH_SEPARATOR_STR);
569
570 // Create the directory if it doesn't exist (with owner-only permissions)
571 asciichat_error_t mkdir_result = platform_mkdir(log_dir, DIR_PERM_PRIVATE);
572 if (mkdir_result != ASCIICHAT_OK) {
573 // Directory creation failed - fall back to temp_dir without subdirectory
574 SAFE_FREE(log_dir);
575 char *result = SAFE_MALLOC(strlen(temp_dir) + 1, char *);
576 if (!result) {
577 return NULL;
578 }
579 safe_snprintf(result, strlen(temp_dir) + 1, "%s", temp_dir);
580 return result;
581 }
582
583 // Verify the directory is writable
584 if (platform_access(log_dir, PLATFORM_ACCESS_WRITE) != 0) {
585 // Directory not writable - fall back to temp_dir
586 SAFE_FREE(log_dir);
587 char *result = SAFE_MALLOC(strlen(temp_dir) + 1, char *);
588 if (!result) {
589 return NULL;
590 }
591 safe_snprintf(result, strlen(temp_dir) + 1, "%s", temp_dir);
592 return result;
593 }
594
595 return log_dir;
596#else
597 // Debug builds: Use repository root for logs.
598 // find_project_root() returns a raw malloc() allocation (to avoid debug_malloc
599 // recursion), but callers free with SAFE_FREE. Re-allocate with SAFE_MALLOC
600 // so the allocator matches.
601 char *repo_root = find_project_root();
602 if (repo_root) {
603 size_t len = strlen(repo_root) + 1;
604 char *result = SAFE_MALLOC(len, char *);
605 if (result) {
606 memcpy(result, repo_root, len);
607 }
608 free(repo_root);
609 return result;
610 }
611
612 // Fallback to current working directory if repo root not found
613 char cwd_buf[PLATFORM_MAX_PATH_LENGTH];
614 if (!platform_get_cwd(cwd_buf, sizeof(cwd_buf))) {
615 return NULL;
616 }
617
618 char *result = SAFE_MALLOC(strlen(cwd_buf) + 1, char *);
619 safe_snprintf(result, strlen(cwd_buf) + 1, "%s", cwd_buf);
620 return result;
621#endif
622}
623
625#ifdef _WIN32
626 // Windows: Try %PROGRAMDATA%\ascii-chat\ first, then fall back to user directories
627 const char *program_data = platform_getenv("PROGRAMDATA");
628 if (program_data && program_data[0] != '\0') {
629 size_t len = strlen(program_data) + strlen("\\ascii-chat\\") + 1;
630 char *system_dir = SAFE_MALLOC(len, char *);
631 if (system_dir) {
632 safe_snprintf(system_dir, len, "%s\\ascii-chat\\", program_data);
633
634 // Try to create directory recursively with public permissions (755 equivalent)
635 asciichat_error_t mkdir_result = platform_mkdir_recursive(system_dir, 0755);
636 if (mkdir_result == ASCIICHAT_OK) {
637 // Directory exists or was created - check if writable
638 if (platform_access(system_dir, PLATFORM_ACCESS_WRITE) == 0) {
639 return system_dir; // System-wide location is writable
640 }
641 }
642 SAFE_FREE(system_dir);
643 }
644 }
645
646 // Fall back to user data directory
647 char *data_dir = get_data_dir();
648 if (data_dir) {
649 // Try to create the directory recursively
651 if (mkdir_result == ASCIICHAT_OK) {
652 if (platform_access(data_dir, PLATFORM_ACCESS_WRITE) == 0) {
653 return data_dir;
654 }
655 }
656 SAFE_FREE(data_dir);
657 }
658
659 return NULL;
660#else
661 // Unix: Try ${INSTALL_PREFIX}/var/ascii-chat/ first (system-wide, Homebrew-aware)
662 // This uses the baked-in install prefix from paths.h (e.g., /opt/homebrew or /usr/local)
663 const char *prefix = ASCIICHAT_INSTALL_PREFIX;
664 size_t system_len = strlen(prefix) + strlen("/var/ascii-chat/") + 1;
665 char *system_dir = SAFE_MALLOC(system_len, char *);
666 if (system_dir) {
667 safe_snprintf(system_dir, system_len, "%s/var/ascii-chat/", prefix);
668
669 // Try to create directory recursively with public permissions (755) for system-wide use
670 // platform_mkdir_recursive creates parent directories as needed
671 asciichat_error_t mkdir_result = platform_mkdir_recursive(system_dir, 0755);
672 if (mkdir_result == ASCIICHAT_OK) {
673 // Directory exists or was created - check if writable
674 if (platform_access(system_dir, PLATFORM_ACCESS_WRITE) == 0) {
675 return system_dir; // System-wide location is writable
676 }
677 }
678 SAFE_FREE(system_dir);
679 }
680
681#ifdef __APPLE__
682 // On macOS, /usr/local is typically user-writable (set up by Homebrew)
683 // Try it if the install prefix is different (e.g., /opt/homebrew on Apple Silicon)
684 if (strcmp(prefix, "/usr/local") != 0) {
685 const char *usr_local_path = "/usr/local/var/ascii-chat/";
686 size_t usr_local_len = strlen(usr_local_path) + 1;
687 char *usr_local_dir = SAFE_MALLOC(usr_local_len, char *);
688 if (usr_local_dir) {
689 safe_snprintf(usr_local_dir, usr_local_len, "%s", usr_local_path);
690
691 asciichat_error_t mkdir_result = platform_mkdir_recursive(usr_local_dir, 0755);
692 if (mkdir_result == ASCIICHAT_OK) {
693 if (platform_access(usr_local_dir, PLATFORM_ACCESS_WRITE) == 0) {
694 return usr_local_dir; // /usr/local is writable
695 }
696 }
697 SAFE_FREE(usr_local_dir);
698 }
699 }
700#endif
701
702 // Fall back to user data directory (XDG_DATA_HOME or ~/.local/share/ascii-chat/)
703 char *data_dir = get_data_dir();
704 if (data_dir) {
705 // Try to create the directory recursively
707 if (mkdir_result == ASCIICHAT_OK) {
708 if (platform_access(data_dir, PLATFORM_ACCESS_WRITE) == 0) {
709 return data_dir;
710 }
711 }
712 SAFE_FREE(data_dir);
713 }
714
715 return NULL;
716#endif
717}
718
719bool path_normalize_copy(const char *path, char *out, size_t out_len) {
720 if (!path || !out || out_len == 0) {
721 SET_ERRNO(ERROR_INVALID_PARAM, "null path or out or out_len is 0");
722 return false;
723 }
724
725 const char *normalized = normalize_path(path);
726 if (!normalized) {
727 return false;
728 }
729
730 size_t len = strlen(normalized);
731 if (len + 1 > out_len) {
732 return false;
733 }
734
735 memcpy(out, normalized, len + 1);
736 return true;
737}
738
739bool path_is_absolute(const char *path) {
740 if (!path || !*path) {
741 return false;
742 }
743
744#ifdef _WIN32
745 if ((path[0] == '\\' && path[1] == '\\')) {
746 return true; // UNC path
747 }
748 if (isalpha((unsigned char)path[0]) && path[1] == PATH_DRIVE_SEPARATOR && path[2] == PATH_DELIM) {
749 return true;
750 }
751 return false;
752#else
753 return path[0] == PATH_DELIM;
754#endif
755}
756
757bool path_is_within_base(const char *path, const char *base) {
758 if (!path || !base) {
759 return false;
760 }
761
762 if (!path_is_absolute(path) || !path_is_absolute(base)) {
763 return false;
764 }
765
766 char normalized_path[PLATFORM_MAX_PATH_LENGTH];
767 char normalized_base[PLATFORM_MAX_PATH_LENGTH];
768
769 if (!path_normalize_copy(path, normalized_path, sizeof(normalized_path))) {
770 return false;
771 }
772 if (!path_normalize_copy(base, normalized_base, sizeof(normalized_base))) {
773 return false;
774 }
775
776 size_t base_len = strlen(normalized_base);
777 if (base_len == 0) {
778 return false;
779 }
780
781 if (platform_path_strcasecmp(normalized_path, normalized_base, base_len) != 0) {
782 return false;
783 }
784 char next = normalized_path[base_len];
785 if (next == '\0') {
786 return true;
787 }
788 return next == PATH_DELIM;
789}
790
791bool path_is_within_any_base(const char *path, const char *const *bases, size_t base_count) {
792 if (!path || !bases || base_count == 0) {
793 return false;
794 }
795
796 for (size_t i = 0; i < base_count; ++i) {
797 const char *base = bases[i];
798 if (!base) {
799 continue;
800 }
801 if (path_is_within_base(path, base)) {
802 return true;
803 }
804 }
805
806 return false;
807}
808
809bool path_looks_like_path(const char *value) {
810 if (!value || *value == '\0') {
811 return false;
812 }
813
814 if (value[0] == PATH_DELIM || value[0] == PATH_COMPONENT_DOT || value[0] == PATH_TILDE) {
815 return true;
816 }
817
818 if (strchr(value, PATH_DELIM)) {
819 return true;
820 }
821
822#ifdef _WIN32
823 if (isalpha((unsigned char)value[0]) && value[1] == ':' && value[2] == PATH_DELIM) {
824 return true;
825 }
826#endif
827
828 return false;
829}
830
831static asciichat_error_t map_role_to_error(path_role_t role) {
832 switch (role) {
834 return ERROR_CONFIG;
836 return ERROR_LOGGING_INIT;
840 return ERROR_CRYPTO_KEY;
841 }
842 return ERROR_GENERAL;
843}
844
845static void append_base_if_valid(const char *candidate, const char **bases, size_t *count) {
846 if (!candidate || *candidate == '\0' || *count >= MAX_PATH_BASES) {
847 return;
848 }
849 if (!path_is_absolute(candidate)) {
850 return;
851 }
852 bases[*count] = candidate;
853 (*count)++;
854}
855
856static void build_ascii_chat_path(const char *base, const char *suffix, char *out, size_t out_len) {
857 if (!base || !suffix || out_len == 0) {
858 out[0] = '\0';
859 return;
860 }
861
862 size_t base_len = strlen(base);
863 bool needs_sep = base_len > 0 && base[base_len - 1] != PATH_DELIM;
864
865 safe_snprintf(out, out_len, "%s%s%s", base, needs_sep ? PATH_SEPARATOR_STR : "", suffix);
866}
867
875static bool is_sensitive_system_path(const char *path) {
876 if (!path) {
877 return false;
878 }
879
880#ifdef _WIN32
881 // Windows system directories
882 const char *sensitive_paths[] = {"C:\\Windows", // System directory
883 "C:\\Program Files", // Program files
884 "C:\\Program Files (x86)", // 32-bit programs
885 "C:\\ProgramData", // All users data
886 "C:\\System Volume Information", // System recovery
887 "C:\\PerfLogs", // Performance logs
888 NULL};
889#else
890 // Unix/Linux/macOS system directories
891 const char *sensitive_paths[] = {"/etc", // System configuration
892 "/bin", // Essential binaries
893 "/sbin", // System binaries
894 "/usr/bin", // User binaries
895 "/usr/sbin", // User system binaries
896 "/usr/lib", // System libraries
897 "/lib", // Libraries
898 "/lib64", // 64-bit libraries
899 "/boot", // Boot files
900 "/sys", // System interface
901 "/proc", // Process interface
902 "/dev", // Devices
903 "/root", // Root home (should not write to)
904 "/var/lib", // Variable library data
905 "/var/cache", // Cache data
906 "/var/spool", // Spool data
907 NULL};
908
909#ifdef __APPLE__
910 // macOS-specific system paths
911 const char *macos_paths[] = {"/System", // Core system
912 "/Library", // System library
913 "/Applications", // Bundled apps
914 "/Developer", // Developer tools
915 "/Volumes", // Mounted volumes
916 NULL};
917#endif
918#endif
919
920 // Check each sensitive path
921 for (int i = 0; sensitive_paths[i] != NULL; i++) {
922 const char *base = sensitive_paths[i];
923 size_t base_len = strlen(base);
924
925 // Match if path equals base or starts with base + separator
926 if (strcmp(path, base) == 0) {
927 return true; // Exact match is sensitive
928 }
929 if (strncmp(path, base, base_len) == 0) {
930 // Make sure it's followed by a path separator, not a partial match
931 if (path[base_len] == PATH_DELIM || path[base_len] == '/' || path[base_len] == '\\') {
932 return true;
933 }
934 }
935 }
936
937#ifdef __APPLE__
938 // Check macOS paths
939 for (int i = 0; macos_paths[i] != NULL; i++) {
940 const char *base = macos_paths[i];
941 size_t base_len = strlen(base);
942
943 if (strcmp(path, base) == 0) {
944 return true;
945 }
946 if (strncmp(path, base, base_len) == 0) {
947 if (path[base_len] == PATH_DELIM || path[base_len] == '/' || path[base_len] == '\\') {
948 return true;
949 }
950 }
951 }
952#endif
953
954 return false;
955}
956
964static bool is_file_empty(const char *path) {
965 if (!path) {
966 SET_ERRNO(ERROR_INVALID_PARAM, "is_file_empty: invalid parameter");
967 return false;
968 }
969
970 if (!platform_is_regular_file(path)) {
971 return false;
972 }
973
974 FILE *f = platform_fopen("file_stream", path, "r");
975 if (!f) {
976 return false;
977 }
978
979 int c = fgetc(f);
980 fclose(f);
981 return c == EOF;
982}
983
984static bool is_existing_ascii_chat_log(const char *path) {
985 if (!path) {
986 return false;
987 }
988
989 // Check if path is a regular file (not a directory) using platform abstraction
990 if (!platform_is_regular_file(path)) {
991 return false; // Not a regular file (could be directory, doesn't exist, symlink, etc.)
992 }
993
994 // Try to open and read the first line
995 FILE *f = platform_fopen("file_stream", path, "r");
996 if (!f) {
997 return false; // Can't read file
998 }
999
1000 char buffer[256];
1001 bool is_ascii_chat_log = false;
1002
1003 // Read first line and check for ascii-chat log signature
1004 if (fgets(buffer, sizeof(buffer), f) != NULL) {
1005 char plain[sizeof(buffer)];
1006 strip_ansi_codes(buffer, plain, sizeof(plain));
1007
1008 // ascii-chat logs start with timestamps like: [HH:MM:SS.microseconds] [LEVEL]
1009 // Pattern: [digit][digit]:[digit][digit]:[digit][digit].[digits]
1010 if (plain[0] == '[' && isdigit((unsigned char)plain[1]) && isdigit((unsigned char)plain[2]) &&
1011 plain[3] == ':') {
1012 is_ascii_chat_log = true;
1013 }
1014 }
1015
1016 fclose(f);
1017 return is_ascii_chat_log;
1018}
1019
1020asciichat_error_t path_validate_user_path(const char *input, path_role_t role, char **normalized_out) {
1021 if (!normalized_out) {
1022 return SET_ERRNO(map_role_to_error(role), "path_validate_user_path requires output pointer");
1023 }
1024 *normalized_out = NULL;
1025
1026 if (!input || *input == '\0') {
1027 return SET_ERRNO(map_role_to_error(role), "Path is empty for role %d", role);
1028 }
1029
1030 // SECURITY: For log files, if input is a simple filename (no separators or ..), constrain it to a safe directory
1031 if (role == PATH_ROLE_LOG_FILE) {
1032 // Check if input contains path separators or parent directory references
1033 bool is_simple_filename = true;
1034 for (const char *p = input; *p; p++) {
1035 if (*p == PATH_DELIM || *p == '/' || *p == '\\') {
1036 is_simple_filename = false;
1037 break;
1038 }
1039 }
1040 // Also reject ".." components (even without separators like "..something")
1041 if (strstr(input, "..") != NULL) {
1042 is_simple_filename = false;
1043 }
1044
1045 // If it's a simple filename, resolve it to a safe base directory
1046 if (is_simple_filename) {
1047 // Always prefer current working directory for simple log filenames
1048 // This ensures logs go to where the user is running the command from
1049 char safe_base[PLATFORM_MAX_PATH_LENGTH];
1050
1051 if (!platform_get_cwd(safe_base, sizeof(safe_base))) {
1052 // If cwd fails, try config dir as fallback
1053 char *config_dir = get_config_dir();
1054 if (config_dir) {
1055 SAFE_STRNCPY(safe_base, config_dir, sizeof(safe_base));
1056 SAFE_FREE(config_dir);
1057 } else {
1058 return SET_ERRNO(ERROR_LOGGING_INIT, "Failed to determine safe directory for log file");
1059 }
1060 }
1061
1062 // Build the full path: safe_base + separator + input
1063 size_t base_len = strlen(safe_base);
1064 size_t input_len = strlen(input);
1065 bool needs_sep = base_len > 0 && safe_base[base_len - 1] != PATH_DELIM;
1066 size_t total_len = base_len + (needs_sep ? 1 : 0) + input_len + 1;
1067
1068 if (total_len > PLATFORM_MAX_PATH_LENGTH) {
1069 return SET_ERRNO(ERROR_LOGGING_INIT, "Log file path too long: %s/%s", safe_base, input);
1070 }
1071
1072 char resolved_buf[PLATFORM_MAX_PATH_LENGTH];
1073 safe_snprintf(resolved_buf, sizeof(resolved_buf), "%s%s%s", safe_base, needs_sep ? PATH_SEPARATOR_STR : "",
1074 input);
1075
1076 // Normalize the resolved path
1077 char normalized_buf[PLATFORM_MAX_PATH_LENGTH];
1078 if (!path_normalize_copy(resolved_buf, normalized_buf, sizeof(normalized_buf))) {
1079 return SET_ERRNO(ERROR_LOGGING_INIT, "Failed to normalize log file path: %s", resolved_buf);
1080 }
1081
1082 // Allocate and return the result
1083 char *result = SAFE_MALLOC(strlen(normalized_buf) + 1, char *);
1084 if (!result) {
1085 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate normalized path");
1086 }
1087 safe_snprintf(result, strlen(normalized_buf) + 1, "%s", normalized_buf);
1088 *normalized_out = result;
1089 return ASCIICHAT_OK;
1090 }
1091 // If not a simple filename (contains separators), continue with normal validation below
1092 }
1093
1094 // For non-log-files or log files with path separators, validate as usual
1095 if (role != PATH_ROLE_LOG_FILE && !path_looks_like_path(input)) {
1096 return SET_ERRNO(map_role_to_error(role), "Value does not look like a filesystem path: %s", input);
1097 }
1098
1099 char *expanded = expand_path(input);
1100 if (!expanded) {
1101 return SET_ERRNO(map_role_to_error(role), "Failed to expand path: %s", input);
1102 }
1103
1104 char candidate_buf[PLATFORM_MAX_PATH_LENGTH];
1105 const char *candidate_path = expanded;
1106
1107 if (!path_is_absolute(candidate_path)) {
1108 char cwd_buf[PLATFORM_MAX_PATH_LENGTH];
1109 if (!platform_get_cwd(cwd_buf, sizeof(cwd_buf))) {
1110 SAFE_FREE(expanded);
1111 return SET_ERRNO(map_role_to_error(role), "Failed to determine current working directory");
1112 }
1113
1114 size_t total_len = strlen(cwd_buf) + 1 + strlen(candidate_path) + 1;
1115 if (total_len >= sizeof(candidate_buf)) {
1116 SAFE_FREE(expanded);
1117 return SET_ERRNO(map_role_to_error(role), "Resolved path is too long: %s/%s", cwd_buf, candidate_path);
1118 }
1119 if (strlen(candidate_path) > 0 && candidate_path[0] == PATH_DELIM) {
1120 safe_snprintf(candidate_buf, sizeof(candidate_buf), "%s%s", cwd_buf, candidate_path);
1121 } else {
1122 safe_snprintf(candidate_buf, sizeof(candidate_buf), "%s%c%s", cwd_buf, PATH_DELIM, candidate_path);
1123 }
1124 candidate_path = candidate_buf;
1125 }
1126
1127 char normalized_buf[PLATFORM_MAX_PATH_LENGTH];
1128 if (!path_normalize_copy(candidate_path, normalized_buf, sizeof(normalized_buf))) {
1129 SAFE_FREE(expanded);
1130 return SET_ERRNO(map_role_to_error(role), "Failed to normalize path: %s", candidate_path);
1131 }
1132
1133 if (!path_is_absolute(normalized_buf)) {
1134 SAFE_FREE(expanded);
1135 return SET_ERRNO(map_role_to_error(role), "Normalized path is not absolute: %s", normalized_buf);
1136 }
1137
1138 const char *bases[MAX_PATH_BASES] = {0};
1139 size_t base_count = 0;
1140
1141 // Always add current working directory as an allowed base
1142 // This is critical for log files and other paths relative to where the user runs the command
1143 char cwd_base[PLATFORM_MAX_PATH_LENGTH];
1144 if (platform_get_cwd(cwd_base, sizeof(cwd_base))) {
1145 append_base_if_valid(cwd_base, bases, &base_count);
1146 }
1147
1148 char temp_base[PLATFORM_MAX_PATH_LENGTH];
1149 if (platform_get_temp_dir(temp_base, sizeof(temp_base))) {
1150 append_base_if_valid(temp_base, bases, &base_count);
1151 }
1152
1153 char *config_dir = get_config_dir();
1154 if (config_dir) {
1155 append_base_if_valid(config_dir, bases, &base_count);
1156 }
1157
1158 const char *home_env = platform_get_home_dir();
1159 if (home_env) {
1160 append_base_if_valid(home_env, bases, &base_count);
1161 }
1162
1163 char ascii_chat_home[PLATFORM_MAX_PATH_LENGTH];
1164 if (home_env) {
1165 build_ascii_chat_path(home_env, ".ascii-chat", ascii_chat_home, sizeof(ascii_chat_home));
1166 append_base_if_valid(ascii_chat_home, bases, &base_count);
1167 }
1168
1169#ifndef _WIN32
1170 char ascii_chat_home_tmp[PLATFORM_MAX_PATH_LENGTH];
1171 build_ascii_chat_path("/tmp", ".ascii-chat", ascii_chat_home_tmp, sizeof(ascii_chat_home_tmp));
1172 append_base_if_valid(ascii_chat_home_tmp, bases, &base_count);
1173#endif
1174
1175 char ssh_home[PLATFORM_MAX_PATH_LENGTH];
1176 if (home_env) {
1177 build_ascii_chat_path(home_env, ".ssh", ssh_home, sizeof(ssh_home));
1178 append_base_if_valid(ssh_home, bases, &base_count);
1179 }
1180
1181#ifdef _WIN32
1182 char program_data_logs[PLATFORM_MAX_PATH_LENGTH];
1183 const char *program_data = platform_getenv("PROGRAMDATA");
1184 if (program_data) {
1185 build_ascii_chat_path(program_data, "ascii-chat", program_data_logs, sizeof(program_data_logs));
1186 append_base_if_valid(program_data_logs, bases, &base_count);
1187 }
1188#else
1189 // System-wide config directories (for server deployments)
1190 append_base_if_valid("/etc/ascii-chat", bases, &base_count);
1191 append_base_if_valid("/usr/local/etc/ascii-chat", bases, &base_count);
1192 append_base_if_valid("/var/log", bases, &base_count);
1193 append_base_if_valid("/var/tmp", bases, &base_count);
1194 append_base_if_valid("/tmp", bases, &base_count);
1195#ifdef __APPLE__
1196 // On macOS, /tmp is a symlink to /private/tmp
1197 append_base_if_valid("/private/tmp", bases, &base_count);
1198 // On macOS, all user home directories are under /Users
1199 append_base_if_valid("/Users", bases, &base_count);
1200#endif
1201#endif
1202
1203 // Security check: Reject paths that point to sensitive system files
1204 // This applies to all path roles, not just logs
1205 if (is_sensitive_system_path(normalized_buf)) {
1206 SAFE_FREE(expanded);
1207 if (config_dir) {
1208 SAFE_FREE(config_dir);
1209 }
1210 return SET_ERRNO(map_role_to_error(role), "Cannot write to protected system path: %s", normalized_buf);
1211 }
1212
1213 // For log files, apply special validation rules
1214 if (role == PATH_ROLE_LOG_FILE) {
1215 // Check if path is a regular file (not a directory, not non-existent)
1216 // On macOS, fopen() can succeed on directories, so we must use platform_is_regular_file()
1217 bool is_regular_file = platform_is_regular_file(normalized_buf);
1218
1219 // If a regular file exists, it MUST be an ascii-chat log or empty file to be overwritten
1220 if (is_regular_file && !is_existing_ascii_chat_log(normalized_buf) && !is_file_empty(normalized_buf)) {
1221 SAFE_FREE(expanded);
1222 if (config_dir) {
1223 SAFE_FREE(config_dir);
1224 }
1226 "Cannot overwrite existing non-ascii-chat file: %s\n"
1227 "For safety, ascii-chat will only overwrite its own log files or empty files",
1228 normalized_buf);
1229 }
1230
1231 // If file doesn't exist (or is a directory), check that path is in safe locations
1232 if (!is_regular_file) {
1233 bool allowed = base_count == 0 ? true : path_is_within_any_base(normalized_buf, bases, base_count);
1234 if (!allowed) {
1235 SAFE_FREE(expanded);
1236 if (config_dir) {
1237 SAFE_FREE(config_dir);
1238 }
1240 "Log path %s is outside allowed directories (use -L /tmp/file.log, ~/file.log, or "
1241 "relative/absolute paths in safe locations)",
1242 normalized_buf);
1243 }
1244 }
1245 } else {
1246 // For non-log-file paths, apply standard whitelist validation
1247 bool allowed = base_count == 0 ? true : path_is_within_any_base(normalized_buf, bases, base_count);
1248 if (!allowed) {
1249 SAFE_FREE(expanded);
1250 if (config_dir) {
1251 SAFE_FREE(config_dir);
1252 }
1253 return SET_ERRNO(map_role_to_error(role), "Path %s is outside allowed directories", normalized_buf);
1254 }
1255 }
1256
1257 char *result = SAFE_MALLOC(strlen(normalized_buf) + 1, char *);
1258 if (!result) {
1259 SAFE_FREE(expanded);
1260 if (config_dir) {
1261 SAFE_FREE(config_dir);
1262 }
1263 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate normalized path");
1264 }
1265 safe_snprintf(result, strlen(normalized_buf) + 1, "%s", normalized_buf);
1266 *normalized_out = result;
1267
1268 SAFE_FREE(expanded);
1269 if (config_dir) {
1270 SAFE_FREE(config_dir);
1271 }
1272 return ASCIICHAT_OK;
1273}
⚙️ Common definitions, error codes, macros, and types shared throughout the application
Error and exit codes - unified status values (0-255)
Cross-platform filesystem operations.
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define SAFE_STRCPY(dest, dest_size, src)
Definition common.h:471
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_LOGGING_INIT
Definition error_codes.h:59
@ ERROR_CRYPTO_KEY
Definition error_codes.h:97
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_GENERAL
Definition error_codes.h:52
@ ERROR_CONFIG
Definition error_codes.h:57
@ ERROR_INVALID_PARAM
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#define DIR_PERM_PRIVATE
Directory permission: Private (owner read/write/execute only)
Definition filesystem.h:199
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.
Definition util.c:110
int platform_path_strcasecmp(const char *a, const char *b, size_t n)
Platform-aware path string comparison.
Definition util.c:119
char * platform_get_data_dir(void)
Get the application data directory.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
#define PATH_SEPARATOR_STR
Definition filesystem.h:113
void platform_normalize_path_separators(char *path)
Normalize path separators for the current platform.
Definition util.c:98
int platform_access(const char *pathname, int mode)
Check file/directory access permissions.
#define PATH_DELIM
Platform-specific path separator character.
Definition filesystem.h:112
asciichat_error_t platform_mkdir_recursive(const char *path, int mode)
Create directories recursively (mkdir -p equivalent)
const char * platform_getenv(const char *name)
Get an environment variable value.
Definition wasm/system.c:39
char * platform_strdup(const char *s)
Duplicate string (strdup replacement)
#define PLATFORM_ACCESS_WRITE
Check if file/directory is writable.
bool platform_get_temp_dir(char *temp_dir, size_t path_size)
Get the system temporary directory path.
Definition util.c:74
int platform_is_regular_file(const char *path)
Check if a path is a regular file.
Definition util.c:124
FILE * platform_fopen(const char *name, const char *filename, const char *mode)
Safe file open stream (fopen replacement)
const char * platform_get_home_dir(void)
Get the user's home directory.
Definition util.c:94
char * platform_get_config_dir(void)
Get the application configuration directory.
Definition util.c:104
#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:809
char * get_log_dir(void)
Get log directory path appropriate for current build type.
Definition path.c:537
path_role_t
Classification for user-supplied filesystem paths.
Definition path.h:316
void strip_ansi_codes(const char *input, char *output, size_t output_size)
Strip ANSI escape codes from a string.
#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:739
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
char * expand_path(const char *path)
Expand path with tilde (~) support.
Definition path.c:505
bool path_is_within_base(const char *path, const char *base)
Check whether a path resides within a specified base directory.
Definition path.c:757
char * get_discovery_database_dir(void)
Get discovery service database directory with Homebrew-aware system-wide fallback.
Definition path.c:624
void path_cleanup_thread_locals(void)
Cleanup thread-local path resources.
Definition path.c:85
char * get_config_dir(void)
Get configuration directory path with XDG_CONFIG_HOME support.
Definition path.c:527
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:791
#define PATH_COMPONENT_DOT
Path component: current directory (single dot)
Definition path.h:59
char * get_data_dir(void)
Get application data directory path.
Definition path.c:532
const char * extract_project_relative_path(const char *file)
Extract relative path from an absolute path.
Definition path.c:456
#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:719
@ PATH_ROLE_CONFIG_FILE
Definition path.h:317
@ PATH_ROLE_CLIENT_KEYS
Definition path.h:321
@ PATH_ROLE_KEY_PUBLIC
Definition path.h:320
@ PATH_ROLE_LOG_FILE
Definition path.h:318
@ PATH_ROLE_KEY_PRIVATE
Definition path.h:319
📂 Path Manipulation Utilities
System paths configuration (generated by CMake)
#define ASCIICHAT_INSTALL_PREFIX
Installation prefix.
Definition paths.h:30
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
Cross-platform stat and file type checking macros.
ClangTool/LibTooling compatibility shim for stdbool.h.
#define true
Definition stdbool.h:62
Represents a thread-safe compiled PCRE2 regex singleton.
Definition pcre2.c:22
#define PLATFORM_MAX_PATH_LENGTH
Definition system.c:69
Cross-platform system functions interface for ascii-chat.
🔤 String Manipulation and Shell Escaping Utilities