summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--changelog.md3
-rw-r--r--lib/pure/os.nim49
-rw-r--r--lib/pure/pathnorm.nim21
-rw-r--r--tests/stdlib/tos.nim81
4 files changed, 68 insertions, 86 deletions
diff --git a/changelog.md b/changelog.md
index e48f72023..7fad8f822 100644
--- a/changelog.md
+++ b/changelog.md
@@ -111,6 +111,9 @@ proc enumToString*(enums: openArray[enum]): string =
   (default value: true) that can be set to `false` for better Posix
   interoperability. (Bug #9619.)
 
+- `os.joinPath` and `os.normalizePath` handle edge cases like ``"a/b/../../.."``
+  differently.
+
 
 ### Language additions
 
diff --git a/lib/pure/os.nim b/lib/pure/os.nim
index 6521d827c..1ad276b0a 100644
--- a/lib/pure/os.nim
+++ b/lib/pure/os.nim
@@ -984,36 +984,37 @@ proc normalizePath*(path: var string) {.rtl, extern: "nos$1", tags: [], noNimScr
   ##
   ## Warning: URL-encoded and Unicode attempts at directory traversal are not detected.
   ## Triple dot is not handled.
-  let isAbs = isAbsolute(path)
-  var stack: seq[string] = @[]
-  for p in split(path, {DirSep}):
-    case p
-    of "", ".":
-      continue
-    of "..":
-      if stack.len == 0:
-        if isAbs:
-          discard  # collapse all double dots on absoluta paths
-        else:
+  path = pathnorm.normalizePath(path)
+  when false:
+    let isAbs = isAbsolute(path)
+    var stack: seq[string] = @[]
+    for p in split(path, {DirSep}):
+      case p
+      of "", ".":
+        continue
+      of "..":
+        if stack.len == 0:
+          if isAbs:
+            discard  # collapse all double dots on absoluta paths
+          else:
+            stack.add(p)
+        elif stack[^1] == "..":
           stack.add(p)
-      elif stack[^1] == "..":
-        stack.add(p)
+        else:
+          discard stack.pop()
       else:
-        discard stack.pop()
-    else:
-      stack.add(p)
+        stack.add(p)
 
-  if isAbs:
-    path = DirSep & join(stack, $DirSep)
-  elif stack.len > 0:
-    path = join(stack, $DirSep)
-  else:
-    path = "."
+    if isAbs:
+      path = DirSep & join(stack, $DirSep)
+    elif stack.len > 0:
+      path = join(stack, $DirSep)
+    else:
+      path = "."
 
 proc normalizedPath*(path: string): string {.rtl, extern: "nos$1", tags: [], noNimScript.} =
   ## Returns a normalized path for the current OS. See `<#normalizePath>`_
-  result = path
-  normalizePath(result)
+  result = pathnorm.normalizePath(path)
 
 when defined(Windows) and not defined(nimscript):
   proc openHandle(path: string, followSymlink=true, writeAccess=false): Handle =
diff --git a/lib/pure/pathnorm.nim b/lib/pure/pathnorm.nim
index a33afefbd..4a7d74bf8 100644
--- a/lib/pure/pathnorm.nim
+++ b/lib/pure/pathnorm.nim
@@ -66,19 +66,26 @@ proc addNormalizePath*(x: string; result: var string; state: var int; dirSep = D
     if (state shr 1 == 0) and isSlash(x, b):
       result.add dirSep
       state = state or 1
-    elif result.len > (state and 1) and isDotDot(x, b):
-      var d = result.len
-      # f/..
-      while (d-1) > (state and 1) and result[d-1] notin {DirSep, AltSep}:
-        dec d
-      if d > 0: setLen(result, d-1)
+    elif isDotDot(x, b):
+      if (state shr 1) >= 1:
+        var d = result.len
+        # f/..
+        while (d-1) > (state and 1) and result[d-1] notin {DirSep, AltSep}:
+          dec d
+        if d > 0:
+          setLen(result, d-1)
+          dec state, 2
+      else:
+        if result.len > 0 and result[^1] notin {DirSep, AltSep}:
+          result.add dirSep
+        result.add substr(x, b[0], b[1])
     elif isDot(x, b):
       discard "discard the dot"
     elif b[1] >= b[0]:
       if result.len > 0 and result[^1] notin {DirSep, AltSep}:
         result.add dirSep
       result.add substr(x, b[0], b[1])
-    inc state, 2
+      inc state, 2
 
 proc normalizePath*(path: string; dirSep = DirSep): string =
   ## Example:
diff --git a/tests/stdlib/tos.nim b/tests/stdlib/tos.nim
index 467f64fff..66ca3de33 100644
--- a/tests/stdlib/tos.nim
+++ b/tests/stdlib/tos.nim
@@ -190,61 +190,32 @@ block walkDirRec:
   removeDir("walkdir_test")
 
 block normalizedPath:
-  when defined(posix):
-    block relative:
-      doAssert normalizedPath(".") == "."
-      doAssert normalizedPath("..") == ".."
-      doAssert normalizedPath("../") == ".."
-      doAssert normalizedPath("../..") == "../.."
-      doAssert normalizedPath("../a/..") == ".."
-      doAssert normalizedPath("../a/../") == ".."
-      doAssert normalizedPath("./") == "."
-
-    block absolute:
-      doAssert normalizedPath("/") == "/"
-      doAssert normalizedPath("/.") == "/"
-      doAssert normalizedPath("/..") == "/"
-      doAssert normalizedPath("/../") == "/"
-      doAssert normalizedPath("/../..") == "/"
-      doAssert normalizedPath("/../../") == "/"
-      doAssert normalizedPath("/../../../") == "/"
-      doAssert normalizedPath("/a/b/../../foo") == "/foo"
-      doAssert normalizedPath("/a/b/../../../foo") == "/foo"
-      doAssert normalizedPath("/./") == "/"
-      doAssert normalizedPath("//") == "/"
-      doAssert normalizedPath("///") == "/"
-      doAssert normalizedPath("/a//b") == "/a/b"
-      doAssert normalizedPath("/a///b") == "/a/b"
-      doAssert normalizedPath("/a/b/c/..") == "/a/b"
-      doAssert normalizedPath("/a/b/c/../") == "/a/b"
-
-  else:
-    block relative:
-      doAssert normalizedPath(".") == "."
-      doAssert normalizedPath("..") == ".."
-      doAssert normalizedPath("..\\") == ".."
-      doAssert normalizedPath("..\\..") == "..\\.."
-      doAssert normalizedPath("..\\a\\..") == ".."
-      doAssert normalizedPath("..\\a\\..\\") == ".."
-      doAssert normalizedPath(".\\") == "."
-
-    block absolute:
-      doAssert normalizedPath("\\") == "\\"
-      doAssert normalizedPath("\\.") == "\\"
-      doAssert normalizedPath("\\..") == "\\"
-      doAssert normalizedPath("\\..\\") == "\\"
-      doAssert normalizedPath("\\..\\..") == "\\"
-      doAssert normalizedPath("\\..\\..\\") == "\\"
-      doAssert normalizedPath("\\..\\..\\..\\") == "\\"
-      doAssert normalizedPath("\\a\\b\\..\\..\\foo") == "\\foo"
-      doAssert normalizedPath("\\a\\b\\..\\..\\..\\foo") == "\\foo"
-      doAssert normalizedPath("\\.\\") == "\\"
-      doAssert normalizedPath("\\\\") == "\\"
-      doAssert normalizedPath("\\\\\\") == "\\"
-      doAssert normalizedPath("\\a\\\\b") == "\\a\\b"
-      doAssert normalizedPath("\\a\\\\\\b") == "\\a\\b"
-      doAssert normalizedPath("\\a\\b\\c\\..") == "\\a\\b"
-      doAssert normalizedPath("\\a\\b\\c\\..\\") == "\\a\\b"
+  block relative:
+    doAssert normalizedPath(".") == ""
+    doAssert normalizedPath("..") == ".."
+    doAssert normalizedPath("../") == ".."
+    doAssert normalizedPath("../..") == unixToNativePath"../.."
+    doAssert normalizedPath("../a/..") == ".."
+    doAssert normalizedPath("../a/../") == ".."
+    doAssert normalizedPath("./") == ""
+
+  block absolute:
+    doAssert normalizedPath("/") == unixToNativePath"/"
+    doAssert normalizedPath("/.") == unixToNativePath"/"
+    doAssert normalizedPath("/..") == unixToNativePath"/.."
+    doAssert normalizedPath("/../") == unixToNativePath"/.."
+    doAssert normalizedPath("/../..") == unixToNativePath"/../.."
+    doAssert normalizedPath("/../../") == unixToNativePath"/../.."
+    doAssert normalizedPath("/../../../") == unixToNativePath"/../../.."
+    doAssert normalizedPath("/a/b/../../foo") == unixToNativePath"/foo"
+    doAssert normalizedPath("/a/b/../../../foo") == unixToNativePath"/../foo"
+    doAssert normalizedPath("/./") == unixToNativePath"/"
+    doAssert normalizedPath("//") == unixToNativePath"/"
+    doAssert normalizedPath("///") == unixToNativePath"/"
+    doAssert normalizedPath("/a//b") == unixToNativePath"/a/b"
+    doAssert normalizedPath("/a///b") == unixToNativePath"/a/b"
+    doAssert normalizedPath("/a/b/c/..") == unixToNativePath"/a/b"
+    doAssert normalizedPath("/a/b/c/../") == unixToNativePath"/a/b"
 
 block isHidden:
   when defined(posix):