#
#
#            Nim's Runtime Library
#        (c) Copyright 2015 Andreas Rumpf
#
#    See the file "copying.txt", included in this
#    distribution, for details about the copyright.
#

## This module contains basic operating system facilities like
## retrieving environment variables, working with directories,
## running shell commands, etc.

## .. importdoc:: symlinks.nim, appdirs.nim, dirs.nim, ospaths2.nim

runnableExamples:
  let myFile = "/path/to/my/file.nim"
  assert splitPath(myFile) == (head: "/path/to/my", tail: "file.nim")
  when defined(posix):
    assert parentDir(myFile) == "/path/to/my"
  assert splitFile(myFile) == (dir: "/path/to/my", name: "file", ext: ".nim")
  assert myFile.changeFileExt("c") == "/path/to/my/file.c"

## **See also:**
## * `paths <paths.html>`_ and `files <files.html>`_ modules for high-level file manipulation
## * `osproc module <osproc.html>`_ for process communication beyond
##   `execShellCmd proc`_
## * `uri module <uri.html>`_
## * `distros module <distros.html>`_
## * `dynlib module <dynlib.html>`_
## * `streams module <streams.html>`_
import std/private/ospaths2
export ospaths2

import std/private/oscommon

when supportedSystem:
  import std/private/osfiles
  export osfiles

  import std/private/osdirs
  export osdirs

  import std/private/ossymlinks
  export ossymlinks

import std/private/osappdirs
export osappdirs

include system/inclrtl
import std/private/since

import std/cmdline
export cmdline

import std/[strutils, pathnorm]

when defined(nimPreviewSlimSystem):
  import std/[syncio, assertions, widestrs]

const weirdTarget = defined(nimscript) or defined(js)

since (1, 1):
  const
    invalidFilenameChars* = {'/', '\\', ':', '*', '?', '"', '<', '>', '|', '^', '\0'} ## \
    ## Characters that may produce invalid filenames across Linux, Windows and Mac.
    ## You can check if your filename contains any of these chars and strip them for safety.
    ## Mac bans ``':'``, Linux bans ``'/'``, Windows bans all others.
    invalidFilenames* = [
      "CON", "PRN", "AUX", "NUL",
      "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
      "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"] ## \
    ## Filenames that may be invalid across Linux, Windows, Mac, etc.
    ## You can check if your filename match these and rename it for safety
    ## (Currently all invalid filenames are from Windows only).

when weirdTarget:
  discard
elif defined(windows):
  import std/[winlean, times]
elif defined(posix):
  import std/[posix, times]

  proc toTime(ts: Timespec): times.Time {.inline.} =
    result = initTime(ts.tv_sec.int64, ts.tv_nsec.int)

when weirdTarget:
  {.pragma: noWeirdTarget, error: "this proc is not available on the NimScript/js target".}
else:
  {.pragma: noWeirdTarget.}

when defined(nimscript):
  # for procs already defined in scriptconfig.nim
  template noNimJs(body): untyped = discard
elif defined(js):
  {.pragma: noNimJs, error: "this proc is not available on the js target".}
else:
  {.pragma: noNimJs.}


import std/oserrors
export oserrors
import std/envvars
export envvars

import std/private/osseps
export osseps



proc expandTilde*(path: string): string {.
  tags: [ReadEnvEffect, ReadIOEffect].} =
  ## Expands ``~`` or a path starting with ``~/`` to a full path, replacing
  ## ``~`` with `getHomeDir()`_ (otherwise returns ``path`` unmodified).
  ##
  ## Windows: this is still supported despite the Windows platform not having this
  ## convention; also, both ``~/`` and ``~\`` are handled.
  ##
  ## See also:
  ## * `getHomeDir proc`_
  ## * `getConfigDir proc`_
  ## * `getTempDir proc`_
  ## * `getCurrentDir proc`_
  ## * `setCurrentDir proc`_
  runnableExamples:
    assert expandTilde("~" / "appname.cfg") == getHomeDir() / "appname.cfg"
    assert expandTilde("~/foo/bar") == getHomeDir() / "foo/bar"
    assert expandTilde("/foo/bar") == "/foo/bar"

  if len(path) == 0 or path[0] != '~':
    result = path
  elif len(path) == 1:
    result = getHomeDir()
  elif (path[1] in {DirSep, AltSep}):
    result = getHomeDir() / path.substr(2)
  else:
    # TODO: handle `~bob` and `~bob/` which means home of bob
    result = path

proc quoteShellWindows*(s: string): string {.noSideEffect, rtl, extern: "nosp$1".} =
  ## Quote `s`, so it can be safely passed to Windows API.
  ##
  ## Based on Python's `subprocess.list2cmdline`.
  ## See `this link <https://msdn.microsoft.com/en-us/library/17w5ykft.aspx>`_
  ## for more details.
  let needQuote = {' ', '\t'} in s or s.len == 0
  result = ""
  var backslashBuff = ""
  if needQuote:
    result.add("\"")

  for c in s:
    if c == '\\':
      backslashBuff.add(c)
    elif c == '\"':
      for i in 0..<backslashBuff.len*2:
        result.add('\\')
      backslashBuff.setLen(0)
      result.add("\\\"")
    else:
      if backslashBuff.len != 0:
        result.add(backslashBuff)
        backslashBuff.setLen(0)
      result.add(c)

  if backslashBuff.len > 0:
    result.add(backslashBuff)
  if needQuote:
    result.add(backslashBuff)
    result.add("\"")


proc quoteShellPosix*(s: string): string {.noSideEffect, rtl, extern: "nosp$1".} =
  ## Quote ``s``, so it can be safely passed to POSIX shell.
  const safeUnixChars = {'%', '+', '-', '.', '/', '_', ':', '=', '@',
                         '0'..'9', 'A'..'Z', 'a'..'z'}
  if s.len == 0:
    result = "''"
  elif s.allCharsInSet(safeUnixChars):
    result = s
  else:
    result = "'" & s.replace("'", "'\"'\"'") & "'"

when defined(windows) or defined(posix) or defined(nintendoswitch):
  proc quoteShell*(s: string): string {.noSideEffect, rtl, extern: "nosp$1".} =
    ## Quote ``s``, so it can be safely passed to shell.
    ##
    ## When on Windows, it calls `quoteShellWindows proc`_.
    ## Otherwise, calls `quoteShellPosix proc`_.
    when defined(windows):
      result = quoteShellWindows(s)
    else:
      result = quoteShellPosix(s)

  proc quoteShellCommand*(args: openArray[string]): string =
    ## Concatenates and quotes shell arguments `args`.
    runnableExamples:
      when defined(posix):
        assert quoteShellCommand(["aaa", "", "c d"]) == "aaa '' 'c d'"
      when defined(windows):
        assert quoteShellCommand(["aaa", "", "c d"]) == "aaa \"\" \"c d\""

    # can't use `map` pending https://github.com/nim-lang/Nim/issues/8303
    result = ""
    for i in 0..<args.len:
      if i > 0: result.add " "
      result.add quoteShell(args[i])

when not weirdTarget:
  proc c_system(cmd: cstring): cint {.
    importc: "system", header: "<stdlib.h>".}

  when not defined(windows):
    proc c_free(p: pointer) {.
      importc: "free", header: "<stdlib.h>".}


const
  ExeExts* = ## Platform specific file extension for executables.
    ## On Windows ``["exe", "cmd", "bat"]``, on Posix ``[""]``.
    when defined(windows): ["exe", "cmd", "bat"] else: [""]

when supportedSystem:
  proc findExe*(exe: string, followSymlinks: bool = true;
                extensions: openArray[string]=ExeExts): string {.
    tags: [ReadDirEffect, ReadEnvEffect, ReadIOEffect], noNimJs.} =
    ## Searches for `exe` in the current working directory and then
    ## in directories listed in the ``PATH`` environment variable.
    ##
    ## Returns `""` if the `exe` cannot be found. `exe`
    ## is added the `ExeExts`_ file extensions if it has none.
    ##
    ## If the system supports symlinks it also resolves them until it
    ## meets the actual file. This behavior can be disabled if desired
    ## by setting `followSymlinks = false`.

    if exe.len == 0: return
    template checkCurrentDir() =
      for ext in extensions:
        result = addFileExt(exe, ext)
        if fileExists(result): return
    when defined(posix):
      if '/' in exe: checkCurrentDir()
    else:
      checkCurrentDir()
    let path = getEnv("PATH")
    for candidate in split(path, PathSep):
      if candidate.len == 0: continue
      when defined(windows):
        var x = (if candidate[0] == '"' and candidate[^1] == '"':
                  substr(candidate, 1, candidate.len-2) else: candidate) /
                exe
      else:
        var x = expandTilde(candidate) / exe
      for ext in extensions:
        var x = addFileExt(x, ext)
        if fileExists(x):
          when defined(posix) and not defined(nintendoswitch):
            while followSymlinks: # doubles as if here
              if x.symlinkExists:
                var r = newString(maxSymlinkLen)
                var len = readlink(x.cstring, r.cstring, maxSymlinkLen)
                if len < 0:
                  raiseOSError(osLastError(), exe)
                if len > maxSymlinkLen:
                  r = newString(len+1)
                  len = readlink(x.cstring, r.cstring, len)
                setLen(r, len)
                if isAbsolute(r):
                  x = r
                else:
                  x = parentDir(x) / r
              else:
                break
          return x
    result = ""

  when weirdTarget:
    const times = "fake const"
    template Time(x: untyped): untyped = string

  proc getLastModificationTime*(file: string): times.Time {.rtl, extern: "nos$1", noWeirdTarget.} =
    ## Returns the `file`'s last modification time.
    ##
    ## See also:
    ## * `getLastAccessTime proc`_
    ## * `getCreationTime proc`_
    ## * `fileNewer proc`_
    when defined(posix):
      var res: Stat = default(Stat)
      if stat(file, res) < 0'i32: raiseOSError(osLastError(), file)
      result = res.st_mtim.toTime
    else:
      var f: WIN32_FIND_DATA
      var h = findFirstFile(file, f)
      if h == -1'i32: raiseOSError(osLastError(), file)
      result = fromWinTime(rdFileTime(f.ftLastWriteTime))
      findClose(h)

  proc getLastAccessTime*(file: string): times.Time {.rtl, extern: "nos$1", noWeirdTarget.} =
    ## Returns the `file`'s last read or write access time.
    ##
    ## See also:
    ## * `getLastModificationTime proc`_
    ## * `getCreationTime proc`_
    ## * `fileNewer proc`_
    when defined(posix):
      var res: Stat = default(Stat)
      if stat(file, res) < 0'i32: raiseOSError(osLastError(), file)
      result = res.st_atim.toTime
    else:
      var f: WIN32_FIND_DATA
      var h = findFirstFile(file, f)
      if h == -1'i32: raiseOSError(osLastError(), file)
      result = fromWinTime(rdFileTime(f.ftLastAccessTime))
      findClose(h)

  proc getCreationTime*(file: string): times.Time {.rtl, extern: "nos$1", noWeirdTarget.} =
    ## Returns the `file`'s creation time.
    ##
    ## **Note:** Under POSIX OS's, the returned time may actually be the time at
    ## which the file's attribute's were last modified. See
    ## `here <https://github.com/nim-lang/Nim/issues/1058>`_ for details.
    ##
    ## See also:
    ## * `getLastModificationTime proc`_
    ## * `getLastAccessTime proc`_
    ## * `fileNewer proc`_
    when defined(posix):
      var res: Stat = default(Stat)
      if stat(file, res) < 0'i32: raiseOSError(osLastError(), file)
      result = res.st_ctim.toTime
    else:
      var f: WIN32_FIND_DATA
      var h = findFirstFile(file, f)
      if h == -1'i32: raiseOSError(osLastError(), file)
      result = fromWinTime(rdFileTime(f.ftCreationTime))
      findClose(h)

  proc fileNewer*(a, b: string): bool {.rtl, extern: "nos$1", noWeirdTarget.} =
    ## Returns true if the file `a` is newer than file `b`, i.e. if `a`'s
    ## modification time is later than `b`'s.
    ##
    ## See also:
    ## * `getLastModificationTime proc`_
    ## * `getLastAccessTime proc`_
    ## * `getCreationTime proc`_
    when defined(posix):
      # If we don't have access to nanosecond resolution, use '>='
      when not StatHasNanoseconds:
        result = getLastModificationTime(a) >= getLastModificationTime(b)
      else:
        result = getLastModificationTime(a) > getLastModificationTime(b)
    else:
      result = getLastModificationTime(a) > getLastModificationTime(b)


  proc isAdmin*: bool {.noWeirdTarget.} =
    ## Returns whether the caller's process is a member of the Administrators local
    ## group (on Windows) or a root (on POSIX), via `geteuid() == 0`.
    when defined(windows):
      # Rewrite of the example from Microsoft Docs:
      # https://docs.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-checktokenmembership#examples
      # and corresponding PostgreSQL function:
      # https://doxygen.postgresql.org/win32security_8c.html#ae6b61e106fa5d6c5d077a9d14ee80569
      var ntAuthority = SID_IDENTIFIER_AUTHORITY(value: SECURITY_NT_AUTHORITY)
      var administratorsGroup: PSID
      if not isSuccess(allocateAndInitializeSid(addr ntAuthority,
                                                BYTE(2),
                                                SECURITY_BUILTIN_DOMAIN_RID,
                                                DOMAIN_ALIAS_RID_ADMINS,
                                                0, 0, 0, 0, 0, 0,
                                                addr administratorsGroup)):
        raiseOSError(osLastError(), "could not get SID for Administrators group")

      try:
        var b: WINBOOL
        if not isSuccess(checkTokenMembership(0, administratorsGroup, addr b)):
          raiseOSError(osLastError(), "could not check access token membership")

        result = isSuccess(b)
      finally:
        if freeSid(administratorsGroup) != nil:
          raiseOSError(osLastError(), "failed to free SID for Administrators group")

    else:
      result = geteuid() == 0

  proc expandFilename*(filename: string): string {.rtl, extern: "nos$1",
    tags: [ReadDirEffect], noWeirdTarget.} =
    ## Returns the full (`absolute`:idx:) path of an existing file `filename`.
    ##
    ## Raises `OSError` in case of an error. Follows symlinks.
    result = ""
    when defined(windows):
      var bufsize = MAX_PATH.int32
      var unused: WideCString = nil
      var res = newWideCString(bufsize)
      while true:
        var L = getFullPathNameW(newWideCString(filename), bufsize, res, unused)
        if L == 0'i32:
          raiseOSError(osLastError(), filename)
        elif L > bufsize:
          res = newWideCString(L)
          bufsize = L
        else:
          result = res$L
          break
      # getFullPathName doesn't do case corrections, so we have to use this convoluted
      # way of retrieving the true filename
      for x in walkFiles(result):
        result = x
      if not fileExists(result) and not dirExists(result):
        # consider using: `raiseOSError(osLastError(), result)`
        raise newException(OSError, "file '" & result & "' does not exist")
    else:
      # according to Posix we don't need to allocate space for result pathname.
      # But we need to free return value with free(3).
      var r = realpath(filename, nil)
      if r.isNil:
        raiseOSError(osLastError(), filename)
      else:
        result = $r
        c_free(cast[pointer](r))

  proc createHardlink*(src, dest: string) {.noWeirdTarget.} =
    ## Create a hard link at `dest` which points to the item specified
    ## by `src`.
    ##
    ## .. warning:: Some OS's restrict the creation of hard links to
    ##   root users (administrators).
    ##
    ## See also:
    ## * `createSymlink proc`_
    when defined(windows):
      var wSrc = newWideCString(src)
      var wDst = newWideCString(dest)
      if createHardLinkW(wDst, wSrc, nil) == 0:
        raiseOSError(osLastError(), $(src, dest))
    else:
      if link(src, dest) != 0:
        raiseOSError(osLastError(), $(src, dest))

  proc sleep*(milsecs: int) {.rtl, extern: "nos$1", tags: [TimeEffect], noWeirdTarget.} =
    ## Sleeps `milsecs` milliseconds.
    ## A negative `milsecs` causes sleep to return immediately.
    when defined(windows):
      if milsecs < 0:
        return  # fixes #23732
      winlean.sleep(int32(milsecs))
    else:
      var a, b: Timespec = default(Timespec)
      a.tv_sec = posix.Time(milsecs div 1000)
      a.tv_nsec = (milsecs mod 1000) * 1000 * 1000
      discard posix.nanosleep(a, b)

  proc getFileSize*(file: string): BiggestInt {.rtl, extern: "nos$1",
    tags: [ReadIOEffect], noWeirdTarget.} =
    ## Returns the file size of `file` (in bytes). ``OSError`` is
    ## raised in case of an error.
    when defined(windows):
      var a: WIN32_FIND_DATA
      var resA = findFirstFile(file, a)
      if resA == -1: raiseOSError(osLastError(), file)
      result = rdFileSize(a)
      findClose(resA)
    else:
      var rawInfo: Stat = default(Stat)
      if stat(file, rawInfo) < 0'i32:
        raiseOSError(osLastError(), file)
      rawInfo.st_size

  proc exitStatusLikeShell*(status: cint): cint =
    ## Converts exit code from `c_system` into a shell exit code.
    when defined(posix) and not weirdTarget:
      if WIFSIGNALED(status):
        # like the shell!
        128 + WTERMSIG(status)
      else:
        WEXITSTATUS(status)
    else:
      status

  proc execShellCmd*(command: string): int {.rtl, extern: "nos$1",
    tags: [ExecIOEffect], noWeirdTarget.} =
    ## Executes a `shell command`:idx:.
    ##
    ## Command has the form 'program args' where args are the command
    ## line arguments given to program. The proc returns the error code
    ## of the shell when it has finished (zero if there is no error).
    ## The proc does not return until the process has finished.
    ##
    ## To execute a program without having a shell involved, use `osproc.execProcess proc
    ## <osproc.html#execProcess,string,string,openArray[string],StringTableRef,set[ProcessOption]>`_.
    ##
    ## **Examples:**
    ##   ```Nim
    ##   discard execShellCmd("ls -la")
    ##   ```
    result = exitStatusLikeShell(c_system(command))

  proc inclFilePermissions*(filename: string,
                            permissions: set[FilePermission]) {.
    rtl, extern: "nos$1", tags: [ReadDirEffect, WriteDirEffect], noWeirdTarget.} =
    ## A convenience proc for:
    ##   ```nim
    ##   setFilePermissions(filename, getFilePermissions(filename)+permissions)
    ##   ```
    setFilePermissions(filename, getFilePermissions(filename)+permissions)

  proc exclFilePermissions*(filename: string,
                            permissions: set[FilePermission]) {.
    rtl, extern: "nos$1", tags: [ReadDirEffect, WriteDirEffect], noWeirdTarget.} =
    ## A convenience proc for:
    ##   ```nim
    ##   setFilePermissions(filename, getFilePermissions(filename)-permissions)
    ##   ```
    setFilePermissions(filename, getFilePermissions(filename)-permissions)

when not weirdTarget and (defined(freebsd) or defined(dragonfly) or defined(netbsd)):
  proc sysctl(name: ptr cint, namelen: cuint, oldp: pointer, oldplen: ptr csize_t,
              newp: pointer, newplen: csize_t): cint
       {.importc: "sysctl",header: """#include <sys/types.h>
                                      #include <sys/sysctl.h>""".}
  const
    CTL_KERN = 1
    KERN_PROC = 14
    MAX_PATH = 1024

  when defined(freebsd):
    const KERN_PROC_PATHNAME = 12
  elif defined(netbsd):
    const KERN_PROC_ARGS = 48
    const KERN_PROC_PATHNAME = 5
  else:
    const KERN_PROC_PATHNAME = 9

  proc getApplFreebsd(): string =
    var pathLength = csize_t(0)

    when defined(netbsd):
      var req = [CTL_KERN.cint, KERN_PROC_ARGS.cint, -1.cint, KERN_PROC_PATHNAME.cint]
    else:
      var req = [CTL_KERN.cint, KERN_PROC.cint, KERN_PROC_PATHNAME.cint, -1.cint]

    # first call to get the required length
    var res = sysctl(addr req[0], 4, nil, addr pathLength, nil, 0)

    if res < 0:
      return ""

    result.setLen(pathLength)
    res = sysctl(addr req[0], 4, addr result[0], addr pathLength, nil, 0)

    if res < 0:
      return ""

    let realLen = len(cstring(result))
    setLen(result, realLen)

when not weirdTarget and (defined(linux) or defined(solaris) or defined(bsd) or defined(aix)):
  proc getApplAux(procPath: string): string =
    result = newString(maxSymlinkLen)
    var len = readlink(procPath, result.cstring, maxSymlinkLen)
    if len > maxSymlinkLen:
      result = newString(len+1)
      len = readlink(procPath, result.cstring, len)
    if len < 0: # error in readlink
      len = 0
    setLen(result, len)

when not weirdTarget and defined(openbsd):
  proc getApplOpenBsd(): string =
    # similar to getApplHeuristic, but checks current working directory
    when declared(paramStr):
      result = ""

      # POSIX guaranties that this contains the executable
      # as it has been executed by the calling process
      let exePath = paramStr(0)

      if len(exePath) == 0:
        return ""

      if exePath[0] == DirSep:
        # path is absolute
        result = exePath
      else:
        # not an absolute path, check if it's relative to the current working directory
        for i in 1..<len(exePath):
          if exePath[i] == DirSep:
            result = joinPath(getCurrentDir(), exePath)
            break

      if len(result) > 0:
        return expandFilename(result)

      # search in path
      for p in split(getEnv("PATH"), {PathSep}):
        var x = joinPath(p, exePath)
        if fileExists(x):
          return expandFilename(x)
    else:
      result = ""

when not (defined(windows) or defined(macosx) or weirdTarget) and supportedSystem:
  proc getApplHeuristic(): string =
    when declared(paramStr):
      result = paramStr(0)
      # POSIX guaranties that this contains the executable
      # as it has been executed by the calling process
      if len(result) > 0 and result[0] != DirSep: # not an absolute path?
        # iterate over any path in the $PATH environment variable
        for p in split(getEnv("PATH"), {PathSep}):
          var x = joinPath(p, result)
          if fileExists(x): return x
    else:
      result = ""

when defined(macosx):
  type
    cuint32* {.importc: "unsigned int", nodecl.} = int
    ## This is the same as the type ``uint32_t`` in *C*.

  # a really hacky solution: since we like to include 2 headers we have to
  # define two procs which in reality are the same
  proc getExecPath1(c: cstring, size: var cuint32) {.
    importc: "_NSGetExecutablePath", header: "<sys/param.h>".}
  proc getExecPath2(c: cstring, size: var cuint32): bool {.
    importc: "_NSGetExecutablePath", header: "<mach-o/dyld.h>".}

when defined(haiku):
  const
    PATH_MAX = 1024
    B_FIND_PATH_IMAGE_PATH = 1000

  proc find_path(codePointer: pointer, baseDirectory: cint, subPath: cstring,
                 pathBuffer: cstring, bufferSize: csize_t): int32
                {.importc, header: "<FindDirectory.h>".}

  proc getApplHaiku(): string =
    result = newString(PATH_MAX)

    if find_path(nil, B_FIND_PATH_IMAGE_PATH, nil, result, PATH_MAX) == 0:
      let realLen = len(cstring(result))
      setLen(result, realLen)
    else:
      result = ""

when supportedSystem:
  proc getAppFilename*(): string {.rtl, extern: "nos$1", tags: [ReadIOEffect], noWeirdTarget, raises: [].} =
    ## Returns the filename of the application's executable.
    ## This proc will resolve symlinks.
    ##
    ## Returns empty string when name is unavailable
    ##
    ## See also:
    ## * `getAppDir proc`_
    ## * `getCurrentCompilerExe proc`_

    # Linux: /proc/<pid>/exe
    # Solaris:
    # /proc/<pid>/object/a.out (filename only)
    # /proc/<pid>/path/a.out (complete pathname)
    when defined(windows):
      var bufsize = int32(MAX_PATH)
      var buf = newWideCString(bufsize)
      while true:
        var L = getModuleFileNameW(0, buf, bufsize)
        if L == 0'i32:
          result = "" # error!
          break
        elif L > bufsize:
          buf = newWideCString(L)
          bufsize = L
        else:
          result = buf$L
          break
    elif defined(macosx):
      var size = cuint32(0)
      getExecPath1(nil, size)
      result = newString(int(size))
      if getExecPath2(result.cstring, size):
        result = "" # error!
      if result.len > 0:
        try:
          result = result.expandFilename
        except OSError:
          result = ""
    else:
      when defined(linux) or defined(aix):
        result = getApplAux("/proc/self/exe")
      elif defined(solaris):
        result = getApplAux("/proc/" & $getpid() & "/path/a.out")
      elif defined(genode):
        result = "" # Not supported
      elif defined(freebsd) or defined(dragonfly) or defined(netbsd):
        result = getApplFreebsd()
      elif defined(haiku):
        result = getApplHaiku()
      elif defined(openbsd):
        result = try: getApplOpenBsd() except OSError: ""
      elif defined(nintendoswitch):
        result = ""

      # little heuristic that may work on other POSIX-like systems:
      if result.len == 0:
        result = try: getApplHeuristic() except OSError: ""

  proc getAppDir*(): string {.rtl, extern: "nos$1", tags: [ReadIOEffect], noWeirdTarget.} =
    ## Returns the directory of the application's executable.
    ##
    ## See also:
    ## * `getAppFilename proc`_
    result = splitFile(getAppFilename()).dir

proc getCurrentCompilerExe*(): string {.compileTime.} =
  result = ""
  discard "implemented in the vmops"
  ## Returns the path of the currently running Nim compiler or nimble executable.
  ##
  ## Can be used to retrieve the currently executing
  ## Nim compiler from a Nim or nimscript program, or the nimble binary
  ## inside a nimble program (likewise with other binaries built from
  ## compiler API).

when defined(windows) or weirdTarget:
  type
    DeviceId* = int32
    FileId* = int64
elif defined(posix):
  type
    DeviceId* = Dev
    FileId* = Ino

when defined(js):
  when not declared(FileHandle):
    type FileHandle = distinct int32
  when not declared(File):
    type File = object

when weirdTarget or defined(windows) or defined(posix) or defined(nintendoswitch):
  type
    FileInfo* = object
      ## Contains information associated with a file object.
      ##
      ## See also:
      ## * `getFileInfo(handle) proc`_
      ## * `getFileInfo(file) proc`_
      ## * `getFileInfo(path, followSymlink) proc`_
      id*: tuple[device: DeviceId, file: FileId] ## Device and file id.
      kind*: PathComponent              ## Kind of file object - directory, symlink, etc.
      size*: BiggestInt                 ## Size of file.
      permissions*: set[FilePermission] ## File permissions
      linkCount*: BiggestInt            ## Number of hard links the file object has.
      lastAccessTime*: times.Time       ## Time file was last accessed.
      lastWriteTime*: times.Time        ## Time file was last modified/written to.
      creationTime*: times.Time         ## Time file was created. Not supported on all systems!
      blockSize*: int                   ## Preferred I/O block size for this object.
                                        ## In some filesystems, this may vary from file to file.
      isSpecial*: bool                  ## Is file special? (on Unix some "files"
                                        ## can be special=non-regular like FIFOs,
                                        ## devices); for directories `isSpecial`
                                        ## is always `false`, for symlinks it is
                                        ## the same as for the link's target.

  template rawToFormalFileInfo(rawInfo, path, formalInfo): untyped =
    ## Transforms the native file info structure into the one nim uses.
    ## 'rawInfo' is either a 'BY_HANDLE_FILE_INFORMATION' structure on Windows,
    ## or a 'Stat' structure on posix
    when defined(windows):
      template merge[T](a, b): untyped =
         cast[T](
          (uint64(cast[uint32](a))) or
          (uint64(cast[uint32](b)) shl 32)
         )
      formalInfo.id.device = rawInfo.dwVolumeSerialNumber
      formalInfo.id.file = merge[FileId](rawInfo.nFileIndexLow, rawInfo.nFileIndexHigh)
      formalInfo.size = merge[BiggestInt](rawInfo.nFileSizeLow, rawInfo.nFileSizeHigh)
      formalInfo.linkCount = rawInfo.nNumberOfLinks
      formalInfo.lastAccessTime = fromWinTime(rdFileTime(rawInfo.ftLastAccessTime))
      formalInfo.lastWriteTime = fromWinTime(rdFileTime(rawInfo.ftLastWriteTime))
      formalInfo.creationTime = fromWinTime(rdFileTime(rawInfo.ftCreationTime))
      formalInfo.blockSize = 8192 # xxx use Windows API instead of hardcoding

      # Retrieve basic permissions
      if (rawInfo.dwFileAttributes and FILE_ATTRIBUTE_READONLY) != 0'i32:
        formalInfo.permissions = {fpUserExec, fpUserRead, fpGroupExec,
                                  fpGroupRead, fpOthersExec, fpOthersRead}
      else:
        formalInfo.permissions = {fpUserExec..fpOthersRead}

      # Retrieve basic file kind
      if (rawInfo.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY) != 0'i32:
        formalInfo.kind = pcDir
      else:
        formalInfo.kind = pcFile
      if (rawInfo.dwFileAttributes and FILE_ATTRIBUTE_REPARSE_POINT) != 0'i32:
        formalInfo.kind = succ(formalInfo.kind)

    else:
      template checkAndIncludeMode(rawMode, formalMode: untyped) =
        if (rawInfo.st_mode and rawMode.Mode) != 0.Mode:
          formalInfo.permissions.incl(formalMode)
      formalInfo.id = (rawInfo.st_dev, rawInfo.st_ino)
      formalInfo.size = rawInfo.st_size
      formalInfo.linkCount = rawInfo.st_nlink.BiggestInt
      formalInfo.lastAccessTime = rawInfo.st_atim.toTime
      formalInfo.lastWriteTime = rawInfo.st_mtim.toTime
      formalInfo.creationTime = rawInfo.st_ctim.toTime
      formalInfo.blockSize = rawInfo.st_blksize

      formalInfo.permissions = {}
      checkAndIncludeMode(S_IRUSR, fpUserRead)
      checkAndIncludeMode(S_IWUSR, fpUserWrite)
      checkAndIncludeMode(S_IXUSR, fpUserExec)

      checkAndIncludeMode(S_IRGRP, fpGroupRead)
      checkAndIncludeMode(S_IWGRP, fpGroupWrite)
      checkAndIncludeMode(S_IXGRP, fpGroupExec)

      checkAndIncludeMode(S_IROTH, fpOthersRead)
      checkAndIncludeMode(S_IWOTH, fpOthersWrite)
      checkAndIncludeMode(S_IXOTH, fpOthersExec)

      (formalInfo.kind, formalInfo.isSpecial) =
        if S_ISDIR(rawInfo.st_mode):
          (pcDir, false)
        elif S_ISLNK(rawInfo.st_mode):
          assert(path != "") # symlinks can't occur for file handles
          getSymlinkFileKind(path)
        else:
          (pcFile, not S_ISREG(rawInfo.st_mode))

  proc getFileInfo*(handle: FileHandle): FileInfo {.noWeirdTarget.} =
    ## Retrieves file information for the file object represented by the given
    ## handle.
    ##
    ## If the information cannot be retrieved, such as when the file handle
    ## is invalid, `OSError` is raised.
    ##
    ## See also:
    ## * `getFileInfo(file) proc`_
    ## * `getFileInfo(path, followSymlink) proc`_

    # Done: ID, Kind, Size, Permissions, Link Count
    result = default(FileInfo)
    when defined(windows):
      var rawInfo: BY_HANDLE_FILE_INFORMATION
      # We have to use the super special '_get_osfhandle' call (wrapped in winlean)
      # To transform the C file descriptor to a native file handle.
      var realHandle = get_osfhandle(handle.cint)
      if getFileInformationByHandle(realHandle, addr rawInfo) == 0:
        raiseOSError(osLastError(), $(int handle))
      rawToFormalFileInfo(rawInfo, "", result)
    else:
      var rawInfo: Stat = default(Stat)
      if fstat(handle, rawInfo) < 0'i32:
        raiseOSError(osLastError(), $handle)
      rawToFormalFileInfo(rawInfo, "", result)

  proc getFileInfo*(file: File): FileInfo {.noWeirdTarget.} =
    ## Retrieves file information for the file object.
    ##
    ## See also:
    ## * `getFileInfo(handle) proc`_
    ## * `getFileInfo(path, followSymlink) proc`_
    if file.isNil:
      raise newException(IOError, "File is nil")
    result = getFileInfo(file.getFileHandle())

  proc getFileInfo*(path: string, followSymlink = true): FileInfo {.noWeirdTarget.} =
    ## Retrieves file information for the file object pointed to by `path`.
    ##
    ## Due to intrinsic differences between operating systems, the information
    ## contained by the returned `FileInfo object`_ will be slightly
    ## different across platforms, and in some cases, incomplete or inaccurate.
    ##
    ## When `followSymlink` is true (default), symlinks are followed and the
    ## information retrieved is information related to the symlink's target.
    ## Otherwise, information on the symlink itself is retrieved (however,
    ## field `isSpecial` is still determined from the target on Unix).
    ##
    ## If the information cannot be retrieved, such as when the path doesn't
    ## exist, or when permission restrictions prevent the program from retrieving
    ## file information, `OSError` is raised.
    ##
    ## See also:
    ## * `getFileInfo(handle) proc`_
    ## * `getFileInfo(file) proc`_
    result = default(FileInfo)
    when defined(windows):
      var
        handle = openHandle(path, followSymlink)
        rawInfo: BY_HANDLE_FILE_INFORMATION
      if handle == INVALID_HANDLE_VALUE:
        raiseOSError(osLastError(), path)
      if getFileInformationByHandle(handle, addr rawInfo) == 0:
        raiseOSError(osLastError(), path)
      rawToFormalFileInfo(rawInfo, path, result)
      discard closeHandle(handle)
    else:
      var rawInfo: Stat = default(Stat)
      if followSymlink:
        if stat(path, rawInfo) < 0'i32:
          raiseOSError(osLastError(), path)
      else:
        if lstat(path, rawInfo) < 0'i32:
          raiseOSError(osLastError(), path)
      rawToFormalFileInfo(rawInfo, path, result)

  proc sameFileContent*(path1, path2: string): bool {.rtl, extern: "nos$1",
    tags: [ReadIOEffect], noWeirdTarget.} =
    ## Returns true if both pathname arguments refer to files with identical
    ## binary content.
    ##
    ## See also:
    ## * `sameFile proc`_
    result = false
    var
      a, b: File = default(File)
    if not open(a, path1): return false
    if not open(b, path2):
      close(a)
      return false
    let bufSize = getFileInfo(a).blockSize
    var bufA = alloc(bufSize)
    var bufB = alloc(bufSize)
    while true:
      var readA = readBuffer(a, bufA, bufSize)
      var readB = readBuffer(b, bufB, bufSize)
      if readA != readB:
        result = false
        break
      if readA == 0:
        result = true
        break
      result = equalMem(bufA, bufB, readA)
      if not result: break
      if readA != bufSize: break # end of file
    dealloc(bufA)
    dealloc(bufB)
    close(a)
    close(b)

  proc getCurrentProcessId*(): int {.noWeirdTarget.} =
    ## Return current process ID.
    ##
    ## See also:
    ## * `osproc.processID(p: Process) <osproc.html#processID,Process>`_
    when defined(windows):
      proc GetCurrentProcessId(): DWORD {.stdcall, dynlib: "kernel32",
                                          importc: "GetCurrentProcessId".}
      result = GetCurrentProcessId().int
    else:
      result = getpid()

  proc setLastModificationTime*(file: string, t: times.Time) {.noWeirdTarget.} =
    ## Sets the `file`'s last modification time. `OSError` is raised in case of
    ## an error.
    when defined(posix):
      let unixt = posix.Time(t.toUnix)
      let micro = convert(Nanoseconds, Microseconds, t.nanosecond)
      var timevals = [Timeval(tv_sec: unixt, tv_usec: micro),
        Timeval(tv_sec: unixt, tv_usec: micro)] # [last access, last modification]
      if utimes(file, timevals.addr) != 0: raiseOSError(osLastError(), file)
    else:
      let h = openHandle(path = file, writeAccess = true)
      if h == INVALID_HANDLE_VALUE: raiseOSError(osLastError(), file)
      var ft = t.toWinTime.toFILETIME
      let res = setFileTime(h, nil, nil, ft.addr)
      discard h.closeHandle
      if res == 0'i32: raiseOSError(osLastError(), file)

proc isHidden*(path: string): bool {.noWeirdTarget.} =
  ## Determines whether ``path`` is hidden or not, using `this
  ## reference <https://en.wikipedia.org/wiki/Hidden_file_and_hidden_directory>`_.
  ##
  ## On Windows: returns true if it exists and its "hidden" attribute is set.
  ##
  ## On posix: returns true if ``lastPathPart(path)`` starts with ``.`` and is
  ## not ``.`` or ``..``.
  ##
  ## **Note**: paths are not normalized to determine `isHidden`.
  runnableExamples:
    when defined(posix):
      assert ".foo".isHidden
      assert not ".foo/bar".isHidden
      assert not ".".isHidden
      assert not "..".isHidden
      assert not "".isHidden
      assert ".foo/".isHidden

  when defined(windows):
    wrapUnary(attributes, getFileAttributesW, path)
    if attributes != -1'i32:
      result = (attributes and FILE_ATTRIBUTE_HIDDEN) != 0'i32
  else:
    let fileName = lastPathPart(path)
    result = len(fileName) >= 2 and fileName[0] == '.' and fileName != ".."


func isValidFilename*(filename: string, maxLen = 259.Positive): bool {.since: (1, 1).} =
  ## Returns `true` if `filename` is valid for crossplatform use.
  ##
  ## This is useful if you want to copy or save files across Windows, Linux, Mac, etc.
  ## It uses `invalidFilenameChars`, `invalidFilenames` and `maxLen` to verify the specified `filename`.
  ##
  ## See also:
  ##
  ## * https://docs.microsoft.com/en-us/dotnet/api/system.io.pathtoolongexception
  ## * https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
  ## * https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
  ##
  ## .. warning:: This only checks filenames, not whole paths
  ##    (because basically you can mount anything as a path on Linux).
  runnableExamples:
    assert not isValidFilename(" foo")     # Leading white space
    assert not isValidFilename("foo ")     # Trailing white space
    assert not isValidFilename("foo.")     # Ends with dot
    assert not isValidFilename("con.txt")  # "CON" is invalid (Windows)
    assert not isValidFilename("OwO:UwU")  # ":" is invalid (Mac)
    assert not isValidFilename("aux.bat")  # "AUX" is invalid (Windows)
    assert not isValidFilename("")         # Empty string
    assert not isValidFilename("foo/")     # Filename is empty

  result = true
  let f = filename.splitFile()
  if unlikely(f.name.len + f.ext.len > maxLen or f.name.len == 0 or
    f.name[0] == ' ' or f.name[^1] == ' ' or f.name[^1] == '.' or
    find(f.name, invalidFilenameChars) != -1): return false
  for invalid in invalidFilenames:
    if cmpIgnoreCase(f.name, invalid) == 0: return false


# deprecated declarations
when not weirdTarget:
  template existsFile*(args: varargs[untyped]): untyped {.deprecated: "use fileExists".} =
    fileExists(args)
  template existsDir*(args: varargs[untyped]): untyped {.deprecated: "use dirExists".} =
    dirExists(args)
