1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
|
/* libjodycode: path manipulation
*
* Copyright (C) 2014-2026 by Jody Bruchon <jody@jodybruchon.com>
* Released under The MIT License
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "likely_unlikely.h"
#include "libjodycode.h"
/* Collapse dot-dot and single dot path components
* This code MUST be passed a full file pathname (starting with '/') */
int jc_collapse_dotdot(char * const path)
{
char *p; /* string copy input */
char *out; /* string copy output */
unsigned int i = 0;
if (unlikely(*path != '/')) return JC_ENULL;
p = path; out = path;
while (*p != '\0') {
/* Abort if we're too close to the end of the buffer */
if (unlikely(i >= (JC_PATHBUF_SIZE - 3))) return JC_ECDOTDOT;
/* Skip repeated slashes */
while (*p == '/' && *(p + 1) == '/') {
p++; i++;
}
/* Scan for '/./', '/..', '/.\0' combinations */
if (*p == '/' && *(p + 1) == '.'
&& (*(p + 2) == '.' || *(p + 2) == '/' || *(p + 2) == '\0')) {
/* Check for '../' or terminal '..' */
if (*(p + 2) == '.' && (*(p + 3) == '/' || *(p + 3) == '\0')) {
/* Found a dot-dot; pull everything back to the previous directory */
p += 3; i += 3;
/* If already at root, skip over the dot-dot */
if (i == 0) continue;
/* Don't seek back past the first character */
if ((uintptr_t)out == (uintptr_t)path) continue;
out--;
while (*out != '/') out--;
if (*p == '\0') break;
continue;
} else if (*(p + 2) == '/' || *(p + 2) == '\0') {
/* Found a single dot; seek input ptr past it */
p += 2; i += 2;
if (*p == '\0') break;
continue;
}
/* Fall through: not a dot or dot-dot, just a slash */
}
/* Copy all remaining text */
*out = *p;
p++; out++; i++;
}
/* If only a root slash remains, be sure to keep it */
if ((uintptr_t)out == (uintptr_t)path) {
*out = '/';
out++;
}
/* Output must always be terminated properly */
*out = '\0';
return 0;
}
/* Create a relative symbolic link path for a destination file */
int jc_make_relative_link_name(const char * const src, const char * const dest, char * rel_path)
{
char *p1, *p2;
char *sp, *dp, *ss;
int retval;
if (unlikely(!src || !dest)) return JC_ENULL;
p1 = (char *)malloc(JC_PATHBUF_SIZE + 8);
if (unlikely(p1 == NULL)) return JC_ENOMEM;
p2 = (char *)malloc(JC_PATHBUF_SIZE + 8);
if (unlikely(p2 == NULL)) { free(p1); return JC_ENOMEM; }
/* Get working directory path and prefix to pathnames if needed */
if (*src != '/' || *dest != '/') {
jc_errno = 0;
if (!jc_getcwd(p1, JC_PATHBUF_SIZE)) {
retval = jc_errno;
goto finish;
}
*(p1 + JC_PATHBUF_SIZE - 1) = '\0';
#ifdef ON_WINDOWS
strncat_s(p1, JC_PATHBUF_SIZE, "/", JC_PATHBUF_SIZE - 1);
strncpy_s(p2, JC_PATHBUF_SIZE, p1, JC_PATHBUF_SIZE);
#else
strncat(p1, "/", JC_PATHBUF_SIZE - 1);
strncpy(p2, p1, JC_PATHBUF_SIZE);
#endif
}
/* If an absolute path is provided, use it as-is */
if (*src == '/') *p1 = '\0';
if (*dest == '/') *p2 = '\0';
/* Concatenate working directory to relative paths */
#ifdef ON_WINDOWS
strncat_s(p1, JC_PATHBUF_SIZE, src, JC_PATHBUF_SIZE);
strncat_s(p2, JC_PATHBUF_SIZE, dest, JC_PATHBUF_SIZE);
#else
strncat(p1, src, JC_PATHBUF_SIZE);
strncat(p2, dest, JC_PATHBUF_SIZE);
#endif
/* Collapse . and .. path components */
if (unlikely(jc_collapse_dotdot(p1) != 0 || jc_collapse_dotdot(p2) != 0)) {
retval = JC_ECDOTDOT;
goto finish;
}
/* Find where paths differ, remembering each slash along the way */
sp = p1; dp = p2; ss = p1;
while (*sp == *dp && *sp != '\0' && *dp != '\0') {
if (*sp == '/') ss = sp;
sp++; dp++;
}
/* If paths are 100% identical then the files are the same file */
if (*sp == '\0' && *dp == '\0') {
retval = 1;
goto finish;
}
/* Replace dirs in destination path with dot-dot */
while (*dp != '\0') {
if (*dp == '/') {
*rel_path++ = '.'; *rel_path++ = '.'; *rel_path++ = '/';
}
dp++;
}
/* Copy the file name into rel_path and return */
ss++;
while (unlikely(*ss != '\0')) *rel_path++ = *ss++;
/* . and .. dirs at end are invalid */
if (*(rel_path - 1) == '.')
if (*(rel_path - 2) == '/' || (*(rel_path - 2) == '.' && *(rel_path - 3) == '/') || unlikely(*(rel_path - 1) == '/')) {
retval = JC_EGRNEND;
}
*rel_path = '\0';
finish:
free(p1); free(p2);
return retval;
}
|