summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--lib/pure/os.nim105
-rw-r--r--lib/pure/pathnorm.nim17
-rw-r--r--lib/std/private/ntpath.nim61
-rw-r--r--tests/arc/topt_no_cursor.nim35
-rw-r--r--tests/stdlib/tntpath.nim48
-rw-r--r--tests/stdlib/tos.nim70
-rw-r--r--tests/stdlib/tpathnorm.nim34
7 files changed, 301 insertions, 69 deletions
diff --git a/lib/pure/os.nim b/lib/pure/os.nim
index 82ccd9051..5541e6f15 100644
--- a/lib/pure/os.nim
+++ b/lib/pure/os.nim
@@ -236,6 +236,9 @@ proc `/`*(head, tail: string): string {.noSideEffect, inline.} =
 
   result = joinPath(head, tail)
 
+when doslikeFileSystem:
+  import std/private/ntpath
+
 proc splitPath*(path: string): tuple[head, tail: string] {.
   noSideEffect, rtl, extern: "nos$1".} =
   ## Splits a directory into `(head, tail)` tuple, so that
@@ -258,8 +261,14 @@ proc splitPath*(path: string): tuple[head, tail: string] {.
     assert splitPath("bin") == ("", "bin")
     assert splitPath("") == ("", "")
 
+  when doslikeFileSystem:
+    let (drive, splitpath) = splitDrive(path)
+    let stop = drive.len
+  else:
+    const stop = 0
+
   var sepPos = -1
-  for i in countdown(len(path)-1, 0):
+  for i in countdown(len(path)-1, stop):
     if path[i] in {DirSep, AltSep}:
       sepPos = i
       break
@@ -272,8 +281,12 @@ proc splitPath*(path: string): tuple[head, tail: string] {.
     )
     result.tail = substr(path, sepPos+1)
   else:
-    result.head = ""
-    result.tail = path
+    when doslikeFileSystem:
+      result.head = drive
+      result.tail = splitpath
+    else:
+      result.head = ""
+      result.tail = path
 
 proc isAbsolute*(path: string): bool {.rtl, noSideEffect, extern: "nos$1", raises: [].} =
   ## Checks whether a given `path` is absolute.
@@ -318,9 +331,6 @@ when doslikeFileSystem:
      (path[0] == DirSep and
       (path.len == 1 or path[1] notin {DirSep, AltSep, ':'})))
 
-  proc isUNCPrefix(path: string): bool {.noSideEffect, raises: [].} =
-    path[0] == DirSep and path[1] == DirSep
-
   proc sameRoot(path1, path2: string): bool {.noSideEffect, raises: [].} =
     ## Return true if path1 and path2 have a same root.
     ##
@@ -330,40 +340,12 @@ when doslikeFileSystem:
     assert(isAbsolute(path1))
     assert(isAbsolute(path2))
 
-    let
-      len1 = path1.len
-      len2 = path2.len
-    assert(len1 != 0 and len2 != 0)
-
     if isAbsFromCurrentDrive(path1) and isAbsFromCurrentDrive(path2):
-      return true
-    elif len1 == 1 or len2 == 1:
-      return false
+      result = true
+    elif cmpIgnoreCase(splitDrive(path1).drive, splitDrive(path2).drive) == 0:
+      result = true
     else:
-      if path1[1] == ':' and path2[1] == ':':
-        return path1[0].toLowerAscii() == path2[0].toLowerAscii()
-      else:
-        var
-          p1, p2: PathIter
-          pp1 = next(p1, path1)
-          pp2 = next(p2, path2)
-        if pp1[1] - pp1[0] == 1 and pp2[1] - pp2[0] == 1 and
-           isUNCPrefix(path1) and isUNCPrefix(path2):
-          #UNC
-          var h = 0
-          while p1.hasNext(path1) and p2.hasNext(path2) and h < 2:
-            pp1 = next(p1, path1)
-            pp2 = next(p2, path2)
-            let diff = pp1[1] - pp1[0]
-            if diff != pp2[1] - pp2[0]:
-              return false
-            for i in 0..diff:
-              if path1[i + pp1[0]] !=? path2[i + pp2[0]]:
-                return false
-            inc h
-          return h == 2
-        else:
-          return false
+      result = false
 
 proc relativePath*(path, base: string, sep = DirSep): string {.
   rtl, extern: "nos$1".} =
@@ -384,7 +366,8 @@ proc relativePath*(path, base: string, sep = DirSep): string {.
   runnableExamples:
     assert relativePath("/Users/me/bar/z.nim", "/Users/other/bad", '/') == "../../me/bar/z.nim"
     assert relativePath("/Users/me/bar/z.nim", "/Users/other", '/') == "../me/bar/z.nim"
-    assert relativePath("/Users///me/bar//z.nim", "//Users/", '/') == "me/bar/z.nim"
+    when not doslikeFileSystem: # On Windows, UNC-paths start with `//`
+      assert relativePath("/Users///me/bar//z.nim", "//Users/", '/') == "me/bar/z.nim"
     assert relativePath("/Users/me/bar/z.nim", "/Users/me", '/') == "bar/z.nim"
     assert relativePath("", "/users/moo", '/') == ""
     assert relativePath("foo", ".", '/') == "foo"
@@ -497,6 +480,9 @@ proc parentDir*(path: string): string {.
       assert parentDir("a//./") == "."
       assert parentDir("a/b/c/..") == "a"
   result = pathnorm.normalizePath(path)
+  when doslikeFileSystem:
+    let (drive, splitpath) = splitDrive(result)
+    result = splitpath
   var sepPos = parentDirPos(result)
   if sepPos >= 0:
     result = substr(result, 0, sepPos)
@@ -507,6 +493,13 @@ proc parentDir*(path: string): string {.
     result = ""
   else:
     result = "."
+  when doslikeFileSystem:
+    if result.len == 0:
+      discard
+    elif drive.len > 0 and result.len == 1 and result[0] in {DirSep, AltSep}:
+      result = drive
+    else:
+      result = drive & result
 
 proc tailDir*(path: string): string {.
   noSideEffect, rtl, extern: "nos$1".} =
@@ -526,6 +519,10 @@ proc tailDir*(path: string): string {.
     assert tailDir("usr/local/bin") == "local/bin"
 
   var i = 0
+  when doslikeFileSystem:
+    let (drive, splitpath) = path.splitDrive
+    if drive != "":
+      return splitpath.strip(chars = {DirSep, AltSep}, trailing = false)
   while i < len(path):
     if path[i] in {DirSep, AltSep}:
       while i < len(path) and path[i] in {DirSep, AltSep}: inc i
@@ -544,6 +541,9 @@ proc isRootDir*(path: string): bool {.
     assert not isRootDir("/a")
     assert not isRootDir("a/b/c")
 
+  when doslikeFileSystem:
+    if splitDrive(path).path == "":
+      return true
   result = parentDirPos(path) < 0
 
 iterator parentDirs*(path: string, fromRoot=false, inclusive=true): string =
@@ -588,7 +588,11 @@ iterator parentDirs*(path: string, fromRoot=false, inclusive=true): string =
       current = current.parentDir
       yield current
   else:
-    for i in countup(0, path.len - 2): # ignore the last /
+    when doslikeFileSystem:
+      let start = path.splitDrive.drive.len
+    else:
+      const start = 0
+    for i in countup(start, path.len - 2): # ignore the last /
       # deal with non-normalized paths such as /foo//bar//baz
       if path[i] in {DirSep, AltSep} and
           (i == 0 or path[i-1] notin {DirSep, AltSep}):
@@ -608,11 +612,15 @@ proc `/../`*(head, tail: string): string {.noSideEffect.} =
       assert "a/b/c" /../ "d/e" == "a/b/d/e"
       assert "a" /../ "d/e" == "a/d/e"
 
+  when doslikeFileSystem:
+    let (drive, head) = splitDrive(head)
   let sepPos = parentDirPos(head)
   if sepPos >= 0:
     result = substr(head, 0, sepPos-1) / tail
   else:
     result = head / tail
+  when doslikeFileSystem:
+    result = drive / result
 
 proc normExt(ext: string): string =
   if ext == "" or ext[0] == ExtSep: result = ext # no copy needed here
@@ -680,7 +688,13 @@ proc splitFile*(path: string): tuple[dir, name, ext: string] {.
 
   var namePos = 0
   var dotPos = 0
-  for i in countdown(len(path) - 1, 0):
+  when doslikeFileSystem:
+    let (drive, _) = splitDrive(path)
+    let stop = len(drive)
+    result.dir = drive
+  else:
+    const stop = 0
+  for i in countdown(len(path) - 1, stop):
     if path[i] in {DirSep, AltSep} or i == 0:
       if path[i] in {DirSep, AltSep}:
         result.dir = substr(path, 0, if likely(i >= 1): i - 1 else: 0)
@@ -2541,8 +2555,13 @@ proc createDir*(dir: string) {.rtl, extern: "nos$1",
   ## * `moveDir proc`_
   var omitNext = false
   when doslikeFileSystem:
-    omitNext = isAbsolute(dir)
-  for i in 1.. dir.len-1:
+    var start = 1
+    if isAbsolute(dir):
+      omitNext = true
+      start = dir.splitDrive.drive.len + 1
+  else:
+    let start = 1
+  for i in start.. dir.len-1:
     if dir[i] in {DirSep, AltSep}:
       if omitNext:
         omitNext = false
diff --git a/lib/pure/pathnorm.nim b/lib/pure/pathnorm.nim
index 10a2a0b67..a71ae0762 100644
--- a/lib/pure/pathnorm.nim
+++ b/lib/pure/pathnorm.nim
@@ -29,10 +29,6 @@ proc next*(it: var PathIter; x: string): (int, int) =
   if not it.notFirst and x[it.i] in {DirSep, AltSep}:
     # absolute path:
     inc it.i
-    when doslikeFileSystem: # UNC paths have leading `\\`
-      if hasNext(it, x) and x[it.i] == DirSep and
-          it.i+1 < x.len and x[it.i+1] != DirSep:
-        inc it.i
   else:
     while it.i < x.len and x[it.i] notin {DirSep, AltSep}: inc it.i
   if it.i > it.prev:
@@ -56,10 +52,23 @@ proc isDotDot(x: string; bounds: (int, int)): bool =
 proc isSlash(x: string; bounds: (int, int)): bool =
   bounds[1] == bounds[0] and x[bounds[0]] in {DirSep, AltSep}
 
+when doslikeFileSystem:
+  import std/private/ntpath
+
 proc addNormalizePath*(x: string; result: var string; state: var int;
     dirSep = DirSep) =
   ## Low level proc. Undocumented.
 
+  when doslikeFileSystem: # Add Windows drive at start without normalization
+    var x = x
+    if result == "":
+      let (drive, file) = splitDrive(x)
+      x = file
+      result.add drive
+      for c in result.mitems:
+        if c in {DirSep, AltSep}:
+          c = dirSep
+
   # state: 0th bit set if isAbsolute path. Other bits count
   # the number of path components.
   var it: PathIter
diff --git a/lib/std/private/ntpath.nim b/lib/std/private/ntpath.nim
new file mode 100644
index 000000000..7c8661bb7
--- /dev/null
+++ b/lib/std/private/ntpath.nim
@@ -0,0 +1,61 @@
+# This module is inspired by Python's `ntpath.py` module.
+
+import std/[
+  strutils,
+]
+
+# Adapted `splitdrive` function from the following commits in Python source
+# code:
+# 5a607a3ee5e81bdcef3f886f9d20c1376a533df4 (2009): Initial UNC handling (by Mark Hammond)
+# 2ba0fd5767577954f331ecbd53596cd8035d7186 (2022): Support for "UNC"-device paths (by Barney Gale)
+#
+# FAQ: Why use `strip` below? `\\?\UNC` is the start of a "UNC symbolic link",
+# which is a special UNC form. Running `strip` differentiates `\\?\UNC\` (a UNC
+# symbolic link) from e.g. `\\?\UNCD` (UNCD is the server in the UNC path).
+func splitDrive*(p: string): tuple[drive, path: string] =
+  ## Splits a Windows path into a drive and path part. The drive can be e.g.
+  ## `C:`. It can also be a UNC path (`\\server\drive\path`).
+  ##
+  ## The equivalent `splitDrive` for POSIX systems always returns empty drive.
+  ## Therefore this proc is only necessary on DOS-like file systems (together
+  ## with Nim's `doslikeFileSystem` conditional variable).
+  ##
+  ## This proc's use case is to extract `path` such that it can be manipulated
+  ## like a POSIX path.
+  runnableExamples:
+    doAssert splitDrive("C:") == ("C:", "")
+    doAssert splitDrive(r"C:\") == (r"C:", r"\")
+    doAssert splitDrive(r"\\server\drive\foo\bar") == (r"\\server\drive", r"\foo\bar")
+    doAssert splitDrive(r"\\?\UNC\server\share\dir") == (r"\\?\UNC\server\share", r"\dir")
+
+  result = ("", p)
+  if p.len < 2:
+    return
+  const sep = '\\'
+  let normp = p.replace('/', sep)
+  if p.len > 2 and normp[0] == sep and normp[1] == sep and normp[2] != sep:
+
+    # is a UNC path:
+    # vvvvvvvvvvvvvvvvvvvv drive letter or UNC path
+    # \\machine\mountpoint\directory\etc\...
+    #           directory ^^^^^^^^^^^^^^^
+    let start = block:
+      const unc = "\\\\?\\UNC" # Length is 7
+      let idx = min(8, normp.len)
+      if unc == normp[0..<idx].strip(chars = {sep}, leading = false).toUpperAscii:
+        8
+      else:
+        2
+    let index = normp.find(sep, start)
+    if index == -1:
+      return
+    var index2 = normp.find(sep, index + 1)
+
+    # a UNC path can't have two slashes in a row (after the initial two)
+    if index2 == index + 1:
+      return
+    if index2 == -1:
+      index2 = p.len
+    return (p[0..<index2], p[index2..^1])
+  if p[1] == ':':
+    return (p[0..1], p[2..^1])
diff --git a/tests/arc/topt_no_cursor.nim b/tests/arc/topt_no_cursor.nim
index f9c789eb9..9585c04a0 100644
--- a/tests/arc/topt_no_cursor.nim
+++ b/tests/arc/topt_no_cursor.nim
@@ -1,5 +1,5 @@
 discard """
-  output: '''(repo: "", package: "meo", ext: "")
+  output: '''(package: "", ext: "meo")
 doing shady stuff...
 3
 6
@@ -11,24 +11,17 @@ doing shady stuff...
   cmd: '''nim c --gc:arc --expandArc:newTarget --expandArc:delete --expandArc:p1 --expandArc:tt --hint:Performance:off --assertions:off --expandArc:extractConfig --expandArc:mergeShadowScope --expandArc:check $file'''
   nimout: '''--expandArc: newTarget
 
-var
-  splat
-  :tmp
-  :tmp_1
-  :tmp_2
-splat = splitFile(path)
-:tmp = splat.dir
-wasMoved(splat.dir)
-:tmp_1 = splat.name
-wasMoved(splat.name)
-:tmp_2 = splat.ext
-wasMoved(splat.ext)
+splat = splitDrive do:
+  let blitTmp = path
+  blitTmp
+:tmp = splat.drive
+wasMoved(splat.drive)
+:tmp_1 = splat.path_1
+wasMoved(splat.path_1)
 result = (
-  let blitTmp = :tmp
-  blitTmp,
-  let blitTmp_1 = :tmp_1
+  let blitTmp_1 = :tmp
   blitTmp_1,
-  let blitTmp_2 = :tmp_2
+  let blitTmp_2 = :tmp_1
   blitTmp_2)
 `=destroy`(splat)
 -- end of expandArc ------------------------
@@ -160,13 +153,13 @@ else:
 -- end of expandArc ------------------------'''
 """
 
-import os
+import os, std/private/ntpath
 
-type Target = tuple[repo, package, ext: string]
+type Target = tuple[package, ext: string]
 
 proc newTarget*(path: string): Target =
-  let splat = path.splitFile
-  result = (repo: splat.dir, package: splat.name, ext: splat.ext)
+  let splat = path.splitDrive
+  result = (package: splat.drive, ext: splat.path)
 
 echo newTarget("meo")
 
diff --git a/tests/stdlib/tntpath.nim b/tests/stdlib/tntpath.nim
new file mode 100644
index 000000000..39798d816
--- /dev/null
+++ b/tests/stdlib/tntpath.nim
@@ -0,0 +1,48 @@
+discard """
+"""
+
+import std/private/ntpath
+
+block: # From Python's `Lib/test/test_ntpath.py`
+  doAssert splitDrive(r"c:\foo\bar") == (r"c:", r"\foo\bar")
+  doAssert splitDrive(r"c:/foo/bar") == (r"c:", r"/foo/bar")
+  doAssert splitDrive(r"\\conky\mountpoint\foo\bar") == (r"\\conky\mountpoint", r"\foo\bar")
+  doAssert splitDrive(r"//conky/mountpoint/foo/bar") == (r"//conky/mountpoint", r"/foo/bar")
+  doAssert splitDrive(r"\\\conky\mountpoint\foo\bar") == (r"", r"\\\conky\mountpoint\foo\bar")
+  doAssert splitDrive(r"///conky/mountpoint/foo/bar") == (r"", r"///conky/mountpoint/foo/bar")
+  doAssert splitDrive(r"\\conky\\mountpoint\foo\bar") == (r"", r"\\conky\\mountpoint\foo\bar")
+  doAssert splitDrive(r"//conky//mountpoint/foo/bar") == (r"", r"//conky//mountpoint/foo/bar")
+  # Issue #19911: UNC part containing U+0130
+  doAssert splitDrive(r"//conky/MOUNTPOİNT/foo/bar") == (r"//conky/MOUNTPOİNT", r"/foo/bar")
+  # gh-81790: support device namespace, including UNC drives.
+  doAssert splitDrive(r"//?/c:") == (r"//?/c:", r"")
+  doAssert splitDrive(r"//?/c:/") == (r"//?/c:", r"/")
+  doAssert splitDrive(r"//?/c:/dir") == (r"//?/c:", r"/dir")
+  doAssert splitDrive(r"//?/UNC") == (r"", r"//?/UNC")
+  doAssert splitDrive(r"//?/UNC/") == (r"", r"//?/UNC/")
+  doAssert splitDrive(r"//?/UNC/server/") == (r"//?/UNC/server/", r"")
+  doAssert splitDrive(r"//?/UNC/server/share") == (r"//?/UNC/server/share", r"")
+  doAssert splitDrive(r"//?/UNC/server/share/dir") == (r"//?/UNC/server/share", r"/dir")
+  doAssert splitDrive(r"//?/VOLUME{00000000-0000-0000-0000-000000000000}/spam") == (r"//?/VOLUME{00000000-0000-0000-0000-000000000000}", r"/spam")
+  doAssert splitDrive(r"//?/BootPartition/") == (r"//?/BootPartition", r"/")
+
+  doAssert splitDrive(r"\\?\c:") == (r"\\?\c:", r"")
+  doAssert splitDrive(r"\\?\c:\") == (r"\\?\c:", r"\")
+  doAssert splitDrive(r"\\?\c:\dir") == (r"\\?\c:", r"\dir")
+  doAssert splitDrive(r"\\?\UNC") == (r"", r"\\?\UNC")
+  doAssert splitDrive(r"\\?\UNC\") == (r"", r"\\?\UNC\")
+  doAssert splitDrive(r"\\?\UNC\server\") == (r"\\?\UNC\server\", r"")
+  doAssert splitDrive(r"\\?\UNC\server\share") == (r"\\?\UNC\server\share", r"")
+  doAssert splitDrive(r"\\?\UNC\server\share\dir") == (r"\\?\UNC\server\share", r"\dir")
+  doAssert splitDrive(r"\\?\VOLUME{00000000-0000-0000-0000-000000000000}\spam") == (r"\\?\VOLUME{00000000-0000-0000-0000-000000000000}", r"\spam")
+  doAssert splitDrive(r"\\?\BootPartition\") == (r"\\?\BootPartition", r"\")
+
+block:
+  doAssert splitDrive(r"C:") == (r"C:", r"")
+  doAssert splitDrive(r"C:\") == (r"C:", r"\")
+  doAssert splitDrive(r"non/absolute/path") == (r"", r"non/absolute/path")
+
+  # Special for `\`-rooted paths on Windows. I don't know if this is correct,
+  # rbut `\` is not recognized as a drive, in contrast to `C:` or `\?\c:`.
+  # This behavior is the same for Python's `splitdrive` function.
+  doAssert splitDrive(r"\\") == (r"", r"\\")
diff --git a/tests/stdlib/tos.nim b/tests/stdlib/tos.nim
index b7816fd41..08088d707 100644
--- a/tests/stdlib/tos.nim
+++ b/tests/stdlib/tos.nim
@@ -508,7 +508,11 @@ block ospaths:
   doAssert relativePath("/Users/me/bar/z.nim", "/Users/other/bad", '/') == "../../me/bar/z.nim"
 
   doAssert relativePath("/Users/me/bar/z.nim", "/Users/other", '/') == "../me/bar/z.nim"
-  doAssert relativePath("/Users///me/bar//z.nim", "//Users/", '/') == "me/bar/z.nim"
+
+  # `//` is a UNC path, `/` is the current working directory's drive, so can't
+  # run this test on Windows.
+  when not doslikeFileSystem:
+    doAssert relativePath("/Users///me/bar//z.nim", "//Users/", '/') == "me/bar/z.nim"
   doAssert relativePath("/Users/me/bar/z.nim", "/Users/me", '/') == "bar/z.nim"
   doAssert relativePath("", "/users/moo", '/') == ""
   doAssert relativePath("foo", "", '/') == "foo"
@@ -707,3 +711,67 @@ block: # isAdmin
   if isAzure and defined(windows): doAssert isAdmin()
   # In Azure on POSIX tests run as a normal user
   if isAzure and defined(posix): doAssert not isAdmin()
+
+when doslikeFileSystem:
+  import std/[sequtils, private/ntpath]
+
+  block: # Bug #19103 UNC paths
+
+    # Easiest way of generating a valid, readable and writable UNC path
+    let tempDir = r"\\?\" & getTempDir()
+    doAssert dirExists tempDir
+    createDir tempDir / "test"
+    removeDir tempDir / "test"
+    createDir tempDir / "recursive" / "test"
+    removeDir tempDir / "recursive" / "test"
+
+    let tempDir2 = getTempDir()
+    let (drive, pathNoDrive) = splitDrive(tempDir2)
+    setCurrentDir drive
+    doAssert cmpIgnoreCase(getCurrentDir().splitDrive.drive, drive) == 0
+
+    # Test `\Users` path syntax on Windows by stripping away drive. `\`
+    # resolves to the drive in current working directory. This drive will be
+    # the same as `tempDir2` because of the `setCurrentDir` above.
+    doAssert pathNoDrive[0] == '\\'
+    createDir pathNoDrive / "test"
+    doAssert dirExists pathNoDrive / "test"
+    removeDir pathNoDrive / "test"
+
+    doAssert splitPath("//?/c:") == ("//?/c:", "")
+
+    doAssert relativePath("//?/c:///Users//me", "//?/c:", '/') == "Users/me"
+
+    doAssert parentDir(r"\\?\c:") == r""
+    doAssert parentDir(r"//?/c:/Users") == r"\\?\c:"
+    doAssert parentDir(r"\\localhost\c$") == r""
+    doAssert parentDir(r"\Users") == r"\"
+
+    doAssert tailDir("//?/c:") == ""
+    doAssert tailDir("//?/c:/Users") == "Users"
+    doAssert tailDir(r"\\localhost\c$\Windows\System32") == r"Windows\System32"
+
+    doAssert isRootDir("//?/c:")
+    doAssert isRootDir("//?/UNC/localhost/c$")
+    doAssert not isRootDir(r"\\?\c:\Users")
+
+    doAssert parentDirs(r"C:\Users", fromRoot = true).toSeq == @[r"C:\", r"C:\Users"]
+    doAssert parentDirs(r"C:\Users", fromRoot = false).toSeq == @[r"C:\Users", r"C:"]
+    doAssert parentDirs(r"\\?\c:\Users", fromRoot = true).toSeq ==
+      @[r"\\?\c:\", r"\\?\c:\Users"]
+    doAssert parentDirs(r"\\?\c:\Users", fromRoot = false).toSeq ==
+      @[r"\\?\c:\Users", r"\\?\c:"]
+    doAssert parentDirs(r"//localhost/c$/Users", fromRoot = true).toSeq ==
+      @[r"//localhost/c$/", r"//localhost/c$/Users"]
+    doAssert parentDirs(r"//?/UNC/localhost/c$/Users", fromRoot = false).toSeq ==
+      @[r"//?/UNC/localhost/c$/Users", r"\\?\UNC\localhost\c$"]
+    doAssert parentDirs(r"\Users", fromRoot = true).toSeq == @[r"\", r"\Users"]
+    doAssert parentDirs(r"\Users", fromRoot = false).toSeq == @[r"\Users", r"\"]
+
+    doAssert r"//?/c:" /../ "d/e" == r"\\?\c:\d\e"
+    doAssert r"//?/c:/Users" /../ "d/e" == r"\\?\c:\d\e"
+    doAssert r"\\localhost\c$" /../ "d/e" == r"\\localhost\c$\d\e"
+
+    doAssert splitFile("//?/c:") == ("//?/c:", "", "")
+    doAssert splitFile("//?/c:/Users") == ("//?/c:", "Users", "")
+    doAssert splitFile(r"\\localhost\c$\test.txt") == (r"\\localhost\c$", "test", ".txt")
diff --git a/tests/stdlib/tpathnorm.nim b/tests/stdlib/tpathnorm.nim
new file mode 100644
index 000000000..2cb644e3c
--- /dev/null
+++ b/tests/stdlib/tpathnorm.nim
@@ -0,0 +1,34 @@
+discard """
+"""
+
+import std/os
+
+when doslikeFileSystem:
+  import std/pathnorm
+
+  template initVars =
+    var state {.inject.} = 0
+    var result {.inject.}: string
+
+  block: # / -> /
+    initVars
+    addNormalizePath("//?/c:/./foo//bar/../baz", result, state, '/')
+    doAssert result == "//?/c:/foo/baz"
+    addNormalizePath("me", result, state, '/')
+    doAssert result == "//?/c:/foo/baz/me"
+
+  block: # / -> \
+    initVars
+    addNormalizePath(r"//?/c:/./foo//bar/../baz", result, state, '\\')
+    doAssert result == r"\\?\c:\foo\baz"
+    addNormalizePath("me", result, state, '\\')
+    doAssert result == r"\\?\c:\foo\baz\me"
+
+  block: # Append path component to UNC drive
+    initVars
+    addNormalizePath(r"//?/c:", result, state, '\\')
+    doAssert result == r"\\?\c:"
+    addNormalizePath("Users", result, state, '\\')
+    doAssert result == r"\\?\c:\Users"
+    addNormalizePath("me", result, state, '\\')
+    doAssert result == r"\\?\c:\Users\me"
lt'>
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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239