3d91dda86ff75aa59b6095ae6bcab96d72f99656 jcasper Wed Mar 18 22:45:15 2026 -0700 eatExcessDotDotInPath does unexpected things with inputs like ../../ and /a/./.. . Adding an alternate version until we can assess the impacts of changing. refs #36320, #37263 diff --git src/lib/osunix.c src/lib/osunix.c index 3ded594ee9f..621632465e5 100644 --- src/lib/osunix.c +++ src/lib/osunix.c @@ -395,46 +395,134 @@ /* Rename file or die trying. */ { int err = rename(oldName, newName); if (err < 0) errnoAbort("Couldn't rename %s to %s", oldName, newName); } void mustRemove(char *path) /* Remove file or die trying */ { int err = remove(path); if (err < 0) errnoAbort("Couldn't remove %s", path); } -static void eatSlashSlashInPath(char *path) +void eatSlashSlashInPath(char *path) /* Convert multiple // to single // */ { char *s, *d; s = d = path; char c, lastC = 0; while ((c = *s++) != 0) { if (c == '/' && lastC == c) continue; *d++ = c; lastC = c; } *d = 0; } +void eatExcessDotsInPath(char *path) +/* Remove . and .. components from path in place using two pointers. + * Single dots are removed, double dots consume the preceding component + * unless it is also ".." or doesn't exist (relative path). + * Assumes no // in input (call eatSlashSlashInPath first). */ +{ +char *src = path; +char *dst = path; +boolean absolute = (*src == '/'); +int pathLen = strlen(path); + +if (absolute) + *dst++ = *src++; + +while (*src) + { + /* Find end of this component */ + char *compEnd = strchr(src, '/'); + int compLen; + if (compEnd) + compLen = compEnd - src; + else + compLen = strlen(src); + + if (compLen == 1 && src[0] == '.') + { + /* Single dot: skip entirely */ + src += compLen; + if (*src == '/') + src++; + } + else if (compLen == 2 && src[0] == '.' && src[1] == '.') + { + /* Double dot: try to consume previous component */ + boolean consumed = FALSE; + if (dst > path + (absolute ? 1 : 0)) + { + /* There is a previous component. Find its start. */ + char *prevEnd = dst - 1; /* points at trailing '/' (or last char) */ + if (prevEnd > path && *(prevEnd) == '/') + prevEnd--; /* back past trailing slash */ + char *prevSlash = matchingCharBeforeInLimits(path, prevEnd + 1, '/'); + char *prevStart; + if (prevSlash == NULL) + prevStart = path; + else + prevStart = prevSlash + 1; + int prevLen = (dst - prevStart); + if (prevStart < dst && *(dst-1) == '/') + prevLen--; /* exclude trailing slash from comparison */ + /* Only consume if previous component is not ".." */ + if (!(prevLen == 2 && prevStart[0] == '.' && prevStart[1] == '.')) + { + dst = prevStart; + /* Also remove the preceding separator if present */ + if (dst > path + (absolute ? 1 : 0) && *(dst-1) == '/') + dst--; + consumed = TRUE; + } + } + if (!consumed) + { + /* Write ".." forward */ + if (dst > path + (absolute ? 1 : 0)) + *dst++ = '/'; + *dst++ = '.'; + *dst++ = '.'; + } + src += compLen; + if (*src == '/') + src++; + } + else + { + /* Normal component: copy with leading slash separator if needed */ + if (dst > path + (absolute ? 1 : 0)) + *dst++ = '/'; + memmove(dst, src, compLen); + dst += compLen; + src += compLen; + if (*src == '/') + src++; + } + } + +*dst = 0; +} + static void eatExcessDotDotInPath(char *path) /* If there's a /.. in path take it out. Turns * 'this/long/../dir/file' to 'this/dir/file * and * 'this/../file' to 'file' * * and * 'this/long/..' to 'this' * and * 'this/..' to '' * and * /this/..' to '/' */ { /* Take out each /../ individually */ for (;;)