diff --git a/lib/pure/os.nim b/lib/pure/os.nim index a1ae4e250e085..43c375746d36d 100644 --- a/lib/pure/os.nim +++ b/lib/pure/os.nim @@ -283,8 +283,11 @@ proc setCurrentDir*(newDir: string) {.inline, tags: [].} = proc expandFilename*(filename: string): string {.rtl, extern: "nos$1", tags: [ReadDirEffect].} = - ## Returns the full (`absolute`:idx:) path of the file `filename`, + ## Returns the full (`absolute`:idx:) path of an existing file `filename`, ## raises OSError in case of an error. + ## + ## To create absolute paths from any path (existing or not) see + ## `<#absolutePath>`_. when defined(windows): var bufsize = MAX_PATH.int32 when useWinUnicode: @@ -323,6 +326,52 @@ proc expandFilename*(filename: string): string {.rtl, extern: "nos$1", result = $r c_free(cast[pointer](r)) +proc normalizePath*(path: string): string = + ## Normalize an UNIX path. + ## + ## Consecutive slashes (/) are collapsed, including an initial double slash. + ## + ## On relative paths, double dot (..) sequences are collapsed if possible. + ## On absolute paths they are always collapsed. + ## + ## Warning: URL-encoded and Unicode attempts at directory traversal are not detected. + ## Triple dot is not handled. + let is_abs = path[0] == DirSep + var stack: seq[string] = @[] + for p in split(path, {DirSep}): + case p + of "", ".": + continue + of "..": + if stack.len == 0: + if is_abs: + discard # collapse all double dots on absoluta paths + else: + stack.add(p) + elif stack[^1] == "..": + stack.add(p) + else: + discard stack.pop() + else: + stack.add(p) + + if is_abs: + result = DirSep & join(stack, $DirSep) + elif stack.len > 0: + result = join(stack, $DirSep) + else: + result = "." + +proc absolutePath*(path: string): string = + ## Returns the normalized, (`absolute`:idx:) version of `path`, based on + ## the current directory. See `<#normalizePath>`_ + if path.isAbsolute: + result = path + else: + result = joinPath(getCurrentDir(), path) + if result.len > 1 and result[^1] == DirSep: + result = result[0..^2] + when defined(Windows): proc openHandle(path: string, followSymlink=true): Handle = var flags = FILE_FLAG_BACKUP_SEMANTICS or FILE_ATTRIBUTE_NORMAL diff --git a/tests/stdlib/tos.nim b/tests/stdlib/tos.nim index 771dc24562ba8..92e6b1afa01e0 100644 --- a/tests/stdlib/tos.nim +++ b/tests/stdlib/tos.nim @@ -42,11 +42,16 @@ Raises true true true + +[Suite] normalizePath + +[Suite] absolutePath + ''' """ # test os path creation, iteration, and deletion -import os, strutils +import os, strutils, unittest let files = @["these.txt", "are.x", "testing.r", "files.q"] let dirs = @["some", "created", "test", "dirs"] @@ -129,3 +134,54 @@ echo fileExists("../dest/a/b/file.txt") echo fileExists("../dest/a/b/c/fileC.txt") removeDir("../dest") + +suite "normalizePath": + test "normalizePath relative": + check normalizePath(".") == "." + check normalizePath("..") == ".." + check normalizePath("../") == ".." + check normalizePath("../..") == "../.." + check normalizePath("../a/..") == ".." + check normalizePath("../a/../") == ".." + check normalizePath("./") == "." + + test "normalizePath absolute": + check normalizePath("/") == "/" + check normalizePath("/.") == "/" + check normalizePath("/..") == "/" + check normalizePath("/../") == "/" + check normalizePath("/../..") == "/" + check normalizePath("/../../") == "/" + check normalizePath("/../../../") == "/" + check normalizePath("/./") == "/" + check normalizePath("//") == "/" + check normalizePath("///") == "/" + check normalizePath("/a//b") == "/a/b" + check normalizePath("/a///b") == "/a/b" + check normalizePath("/a/b/c/..") == "/a/b" + check normalizePath("/a/b/c/../") == "/a/b" + +when defined(Linux) or defined(osx): + + suite "absolutePath": + setup: + let cwd = getCurrentDir() + + test "absolutePath /": + setCurrentDir("/") + check absolutePath("foo") == "/foo" + check absolutePath("") == "/" + #check absolutePath(".") == "/" + check absolutePath("/") == "/" + + test "absolutePath /tmp": + setCurrentDir("/tmp") + check absolutePath("foo") == "/tmp/foo" + check absolutePath("") == "/tmp" + #check absolutePath(".") == "/tmp" + check absolutePath("/") == "/" + #check absolutePath("foo/..") == "/tmp/foo" + #check absolutePath("../..") == "/tmp/foo" + + teardown: + setCurrentDir(cwd)