20static const char *normalize_path(
const char *path) {
27 int component_count = 0;
28 size_t path_len = strlen(path);
34 const char *pos = path;
35 bool absolute =
false;
39 if (path_len >= 3 && isalpha((
unsigned char)path[0]) && path[1] ==
':' && path[2] ==
PATH_DELIM) {
52 while (*pos ==
'/' || *pos ==
'\\') {
59 const char *component_start = pos;
60 while (*pos && *pos !=
'/' && *pos !=
'\\') {
64 size_t component_len = (size_t)(pos - component_start);
65 if (component_len == 0)
68 if (component_len >=
sizeof(components[0])) {
69 component_len =
sizeof(components[0]) - 1;
79 if (component_count > 0) {
85 memcpy(components[component_count], component_start, component_len);
86 components[component_count][component_len] =
'\0';
92 memcpy(components[component_count], component_start, component_len);
93 components[component_count][component_len] =
'\0';
100 if (absolute && path_len >= 3 && isalpha((
unsigned char)path[0]) && path[1] ==
':') {
101 normalized[out_pos++] = path[0];
102 normalized[out_pos++] =
':';
111 for (
int i = 0; i < component_count; i++) {
115 size_t comp_len = strlen(components[i]);
119 memcpy(normalized + out_pos, components[i], comp_len);
123 normalized[out_pos] =
'\0';
132 const char *normalized = normalize_path(file);
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;
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;
151 while ((found = strstr(search_start, dir)) != NULL) {
153 search_start = found + 1;
157 size_t pos = (size_t)(last_found - normalized);
160 if (best_match == NULL || pos > best_match_pos) {
161 best_match = last_found;
162 best_match_pos = pos;
173 const char *last_sep = strrchr(normalized,
PATH_DELIM);
185 const char *home = NULL;
202 size_t total_len = strlen(home) + strlen(path) + 1;
211 for (
char *p = expanded; *p; p++) {
227 if (appdata && appdata[0] !=
'\0') {
228 size_t len = strlen(appdata) + strlen(
"\\ascii-chat\\") + 1;
238 if (userprofile && userprofile[0] !=
'\0') {
239 size_t len = strlen(userprofile) + strlen(
"\\.ascii-chat\\") + 1;
251 if (xdg_config_home && xdg_config_home[0] !=
'\0') {
252 size_t len = strlen(xdg_config_home) + strlen(
"/ascii-chat/") + 1;
263 if (home && home[0] !=
'\0') {
264 size_t len = strlen(home) + strlen(
"/.ascii-chat/") + 1;
288 char *result =
SAFE_MALLOC(strlen(cwd_buf) + 1,
char *);
297 size_t log_dir_len = strlen(temp_dir) + strlen(
PATH_SEPARATOR_STR) + strlen(
"ascii-chat") + 1;
309 char *result =
SAFE_MALLOC(strlen(temp_dir) + 1,
char *);
321 char *result =
SAFE_MALLOC(strlen(temp_dir) + 1,
char *);
337 char *result =
SAFE_MALLOC(strlen(cwd_buf) + 1,
char *);
347 if (!path || !out || out_len == 0) {
351 const char *normalized = normalize_path(path);
356 size_t len = strlen(normalized);
357 if (len + 1 > out_len) {
361 memcpy(out, normalized, len + 1);
366 if (!path || !*path) {
371 if ((path[0] ==
'\\' && path[1] ==
'\\')) {
384 if (!path || !base) {
402 size_t base_len = strlen(normalized_base);
408 if (_strnicmp(normalized_path, normalized_base, base_len) != 0) {
410 if (strncmp(normalized_path, normalized_base, base_len) != 0) {
414 char next = normalized_path[base_len];
422 if (!path || !bases || base_count == 0) {
426 for (
size_t i = 0; i < base_count; ++i) {
427 const char *base = bases[i];
440 if (!value || *value ==
'\0') {
453 if (isalpha((
unsigned char)value[0]) && value[1] ==
':' && value[2] ==
PATH_DELIM) {
475static void append_base_if_valid(
const char *candidate,
const char **bases,
size_t *count) {
476 if (!candidate || *candidate ==
'\0' || *count >=
MAX_PATH_BASES) {
482 bases[*count] = candidate;
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) {
492 size_t base_len = strlen(base);
493 bool needs_sep = base_len > 0 && base[base_len - 1] !=
PATH_DELIM;
499 if (!normalized_out) {
500 return SET_ERRNO(map_role_to_error(role),
"path_validate_user_path requires output pointer");
502 *normalized_out = NULL;
504 if (!input || *input ==
'\0') {
505 return SET_ERRNO(map_role_to_error(role),
"Path is empty for role %d", role);
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;
519 if (strstr(input,
"..") != NULL) {
520 is_simple_filename =
false;
524 if (is_simple_filename) {
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;
560 char *result =
SAFE_MALLOC(strlen(normalized_buf) + 1,
char *);
564 safe_snprintf(result, strlen(normalized_buf) + 1,
"%s", normalized_buf);
565 *normalized_out = result;
573 return SET_ERRNO(map_role_to_error(role),
"Value does not look like a filesystem path: %s", input);
578 return SET_ERRNO(map_role_to_error(role),
"Failed to expand path: %s", input);
582 const char *candidate_path = expanded;
588 return SET_ERRNO(map_role_to_error(role),
"Failed to determine current working directory");
591 size_t total_len = strlen(cwd_buf) + 1 + strlen(candidate_path) + 1;
592 if (total_len >=
sizeof(candidate_buf)) {
594 return SET_ERRNO(map_role_to_error(role),
"Resolved path is too long: %s/%s", cwd_buf, candidate_path);
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);
601 candidate_path = candidate_buf;
607 return SET_ERRNO(map_role_to_error(role),
"Failed to normalize path: %s", candidate_path);
612 return SET_ERRNO(map_role_to_error(role),
"Normalized path is not absolute: %s", normalized_buf);
616 size_t base_count = 0;
620 append_base_if_valid(cwd_base, bases, &base_count);
625 append_base_if_valid(temp_base, bases, &base_count);
630 append_base_if_valid(config_dir, bases, &base_count);
640 append_base_if_valid(home_env, bases, &base_count);
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);
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);
657 build_ascii_chat_path(home_env,
".ssh", ssh_home,
sizeof(ssh_home));
658 append_base_if_valid(ssh_home, bases, &base_count);
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);
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);
685 return SET_ERRNO(map_role_to_error(role),
"Path %s is outside allowed directories", normalized_buf);
688 char *result =
SAFE_MALLOC(strlen(normalized_buf) + 1,
char *);
696 safe_snprintf(result, strlen(normalized_buf) + 1,
"%s", normalized_buf);
697 *normalized_out = result;
Cross-platform file system operations.
#define SAFE_STRNCPY(dst, src, size)
#define SAFE_MALLOC(size, cast)
#define PLATFORM_MAX_PATH_LENGTH
#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)
#define PATH_DRIVE_SEPARATOR
Path component: Windows drive separator (colon)
bool path_looks_like_path(const char *value)
Determine if a string is likely intended to reference the filesystem.
char * get_log_dir(void)
Get log directory path appropriate for current build type.
path_role_t
Classification for user-supplied filesystem paths.
#define MAX_PATH_BASES
Maximum number of path base directories.
bool path_is_absolute(const char *path)
Determine whether a path is absolute on the current platform.
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.
char * expand_path(const char *path)
Expand path with tilde (~) support.
bool path_is_within_base(const char *path, const char *base)
Check whether a path resides within a specified base directory.
char * get_config_dir(void)
Get configuration directory path with XDG_CONFIG_HOME support.
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.
#define PATH_COMPONENT_DOT
Path component: current directory (single dot)
const char * extract_project_relative_path(const char *file)
Extract relative path from an absolute path.
#define PATH_TILDE
Path component: home directory tilde.
bool path_normalize_copy(const char *path, char *out, size_t out_len)
Normalize a path and copy it into the provided buffer.
📂 Path Manipulation Utilities
Cross-platform system functions interface for ascii-chat.
🔤 String Manipulation and Shell Escaping Utilities