fix #13222: make relativePath more robust and flexible (#13451)

* * relativePath(foo) now works
* relativePath(rel, abs) and relativePath(abs, rel) now work (fixes #13222)
* relativePath, absolutePath, getCurrentDir now available in more targets (eg: vm, nodejs etc)
* fix bug: isAbsolutePath now works with -d:js; add tests
* workaround https://github.com/nim-lang/Nim/issues/13469
* remove `relativePath(path)` overload for now
* add back changelog after rebase
This commit is contained in:
Timothee Cour
2020-04-21 14:53:55 -07:00
committed by GitHub
parent dd24004fab
commit 7ce0358351
4 changed files with 113 additions and 68 deletions

View File

@@ -31,6 +31,12 @@
- The file descriptors created for internal bookkeeping by `ioselector_kqueue`
and `ioselector_epoll` will no longer be leaked to child processes.
- `relativePath(rel, abs)` and `relativePath(abs, rel)` used to silently give wrong results
(see #13222); instead they now use `getCurrentDir` to resolve those cases,
and this can now throw in edge cases where `getCurrentDir` throws.
`relativePath` also now works for js with `-d:nodejs`.
## Language changes
- In newruntime it is now allowed to assign discriminator field without restrictions as long as case object doesn't have custom destructor. Discriminator value doesn't have to be a constant either. If you have custom destructor for case object and you do want to freely assign discriminator fields, it is recommended to refactor object into 2 objects like this:
```nim

View File

@@ -98,6 +98,8 @@ type
include "includes/osseps"
proc absolutePathInternal(path: string): string {.gcsafe.}
proc normalizePathEnd(path: var string, trailingSep = false) =
## Ensures ``path`` has exactly 0 or 1 trailing `DirSep`, depending on
## ``trailingSep``, and taking care of edge cases: it preservers whether
@@ -297,8 +299,12 @@ proc isAbsolute*(path: string): bool {.rtl, noSideEffect, extern: "nos$1", raise
result = path[0] != ':'
elif defined(RISCOS):
result = path[0] == '$'
elif defined(posix):
elif defined(posix) or defined(js):
# `or defined(js)` wouldn't be needed pending https://github.com/nim-lang/Nim/issues/13469
# This works around the problem for posix, but windows is still broken with nim js -d:nodejs
result = path[0] == '/'
else:
doAssert false # if ever hits here, adapt as needed
when FileSystemCaseSensitive:
template `!=?`(a, b: char): bool = a != b
@@ -360,8 +366,8 @@ when doslikeFileSystem:
else:
return false
proc relativePath*(path, base: string; sep = DirSep): string {.
noSideEffect, rtl, extern: "nos$1", raises: [].} =
proc relativePath*(path, base: string, sep = DirSep): string {.
rtl, extern: "nos$1".} =
## Converts `path` to a path relative to `base`.
##
## The `sep` (default: `DirSep <#DirSep>`_) is used for the path normalizations,
@@ -390,6 +396,12 @@ proc relativePath*(path, base: string; sep = DirSep): string {.
var path = path
path.normalizePathAux
base.normalizePathAux
let a1 = isAbsolute(path)
let a2 = isAbsolute(base)
if a1 and not a2:
base = absolutePathInternal(base)
elif a2 and not a1:
path = absolutePathInternal(path)
when doslikeFileSystem:
if isAbsolute(path) and isAbsolute(base):
@@ -1293,60 +1305,67 @@ proc fileNewer*(a, b: string): bool {.rtl, extern: "nos$1", noNimScript.} =
else:
result = getLastModificationTime(a) > getLastModificationTime(b)
proc getCurrentDir*(): string {.rtl, extern: "nos$1", tags: [], noNimScript.} =
## Returns the `current working directory`:idx: i.e. where the built
## binary is run.
##
## So the path returned by this proc is determined at run time.
##
## See also:
## * `getHomeDir proc <#getHomeDir>`_
## * `getConfigDir proc <#getConfigDir>`_
## * `getTempDir proc <#getTempDir>`_
## * `setCurrentDir proc <#setCurrentDir,string>`_
## * `currentSourcePath template <system.html#currentSourcePath.t>`_
## * `getProjectPath proc <macros.html#getProjectPath>`_
when defined(windows):
var bufsize = MAX_PATH.int32
when useWinUnicode:
var res = newWideCString("", bufsize)
while true:
var L = getCurrentDirectoryW(bufsize, res)
if L == 0'i32:
raiseOSError(osLastError())
elif L > bufsize:
res = newWideCString("", L)
bufsize = L
else:
result = res$L
break
when not defined(nimscript):
proc getCurrentDir*(): string {.rtl, extern: "nos$1", tags: [].} =
## Returns the `current working directory`:idx: i.e. where the built
## binary is run.
##
## So the path returned by this proc is determined at run time.
##
## See also:
## * `getHomeDir proc <#getHomeDir>`_
## * `getConfigDir proc <#getConfigDir>`_
## * `getTempDir proc <#getTempDir>`_
## * `setCurrentDir proc <#setCurrentDir,string>`_
## * `currentSourcePath template <system.html#currentSourcePath.t>`_
## * `getProjectPath proc <macros.html#getProjectPath>`_
when defined(nodejs):
var ret: cstring
{.emit: "`ret` = process.cwd();".}
return $ret
elif defined(js):
doAssert false, "use -d:nodejs to have `getCurrentDir` defined"
elif defined(windows):
var bufsize = MAX_PATH.int32
when useWinUnicode:
var res = newWideCString("", bufsize)
while true:
var L = getCurrentDirectoryW(bufsize, res)
if L == 0'i32:
raiseOSError(osLastError())
elif L > bufsize:
res = newWideCString("", L)
bufsize = L
else:
result = res$L
break
else:
result = newString(bufsize)
while true:
var L = getCurrentDirectoryA(bufsize, result)
if L == 0'i32:
raiseOSError(osLastError())
elif L > bufsize:
result = newString(L)
bufsize = L
else:
setLen(result, L)
break
else:
var bufsize = 1024 # should be enough
result = newString(bufsize)
while true:
var L = getCurrentDirectoryA(bufsize, result)
if L == 0'i32:
raiseOSError(osLastError())
elif L > bufsize:
result = newString(L)
bufsize = L
else:
setLen(result, L)
if getcwd(result, bufsize) != nil:
setLen(result, c_strlen(result))
break
else:
var bufsize = 1024 # should be enough
result = newString(bufsize)
while true:
if getcwd(result, bufsize) != nil:
setLen(result, c_strlen(result))
break
else:
let err = osLastError()
if err.int32 == ERANGE:
bufsize = bufsize shl 1
doAssert(bufsize >= 0)
result = newString(bufsize)
else:
raiseOSError(osLastError())
let err = osLastError()
if err.int32 == ERANGE:
bufsize = bufsize shl 1
doAssert(bufsize >= 0)
result = newString(bufsize)
else:
raiseOSError(osLastError())
proc setCurrentDir*(newDir: string) {.inline, tags: [], noNimScript.} =
## Sets the `current working directory`:idx:; `OSError`
@@ -1366,23 +1385,26 @@ proc setCurrentDir*(newDir: string) {.inline, tags: [], noNimScript.} =
else:
if chdir(newDir) != 0'i32: raiseOSError(osLastError(), newDir)
when not weirdTarget:
proc absolutePath*(path: string, root = getCurrentDir()): string {.noNimScript.} =
## Returns the absolute path of `path`, rooted at `root` (which must be absolute;
## default: current directory).
## If `path` is absolute, return it, ignoring `root`.
##
## See also:
## * `normalizedPath proc <#normalizedPath,string>`_
## * `normalizePath proc <#normalizePath,string>`_
runnableExamples:
assert absolutePath("a") == getCurrentDir() / "a"
if isAbsolute(path): path
else:
if not root.isAbsolute:
raise newException(ValueError, "The specified root is not absolute: " & root)
joinPath(root, path)
proc absolutePath*(path: string, root = getCurrentDir()): string =
## Returns the absolute path of `path`, rooted at `root` (which must be absolute;
## default: current directory).
## If `path` is absolute, return it, ignoring `root`.
##
## See also:
## * `normalizedPath proc <#normalizedPath,string>`_
## * `normalizePath proc <#normalizePath,string>`_
runnableExamples:
assert absolutePath("a") == getCurrentDir() / "a"
if isAbsolute(path): path
else:
if not root.isAbsolute:
raise newException(ValueError, "The specified root is not absolute: " & root)
joinPath(root, path)
proc absolutePathInternal(path: string): string =
absolutePath(path, getCurrentDir())
proc normalizePath*(path: var string) {.rtl, extern: "nos$1", tags: [].} =
## Normalize a path.

View File

@@ -5,3 +5,17 @@ import os
block:
doAssert "./foo//./bar/".normalizedPath == "foo/bar"
doAssert relativePath(".//foo/bar", "foo") == "bar"
doAssert "/".isAbsolute
doAssert not "".isAbsolute
doAssert not ".".isAbsolute
doAssert not "foo".isAbsolute
doAssert relativePath("", "bar") == ""
doAssert normalizedPath(".///foo//./") == "foo"
let cwd = getCurrentDir()
let isWindows = '\\' in cwd
# defined(windows) doesn't work with -d:nodejs but should
# these actually break because of that (see https://github.com/nim-lang/Nim/issues/13469)
if not isWindows:
doAssert cwd.isAbsolute
doAssert relativePath(getCurrentDir() / "foo", "bar") == "../foo"

View File

@@ -348,6 +348,9 @@ block ospaths:
doAssert relativePath("", "foo") == ""
doAssert relativePath("././/foo", "foo//./") == "."
doAssert relativePath(getCurrentDir() / "bar", "foo") == "../bar".unixToNativePath
doAssert relativePath("bar", getCurrentDir() / "foo") == "../bar".unixToNativePath
when doslikeFileSystem:
doAssert relativePath(r"c:\foo.nim", r"C:\") == r"foo.nim"
doAssert relativePath(r"c:\foo\bar\baz.nim", r"c:\foo") == r"bar\baz.nim"