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 (;;)