# # # 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 `_ and `files `_ modules for high-level file manipulation ## * `osproc module `_ for process communication beyond ## `execShellCmd proc`_ ## * `uri module `_ ## * `distros module `_ ## * `dynlib module `_ ## * `streams module `_ import std/private/ospaths2 export ospaths2 import std/private/osfiles export osfiles import std/private/osdirs export osdirs import std/private/ossymlinks export ossymlinks import std/private/osappdirs export osappdirs import std/private/oscommon include system/inclrtl import std/private/since import std/cmdline export cmdline import 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 winlean, times elif defined(posix): import posix, times proc toTime(ts: Timespec): times.Time {.inline.} = result = initTime(ts.tv_sec.int64, ts.tv_nsec.int) else: {.error: "OS module not ported to your operating system!".} 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 `_ ## 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.. 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.. 0: result.add " " result.add quoteShell(args[i]) when not weirdTarget: proc c_system(cmd: cstring): cint {. importc: "system", header: "".} when not defined(windows): proc c_free(p: pointer) {. importc: "free", header: "".} const ExeExts* = ## Platform specific file extension for executables. ## On Windows ``["exe", "cmd", "bat"]``, on Posix ``[""]``. when defined(windows): ["exe", "cmd", "bat"] else: [""] 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 not (defined(windows) or 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 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 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 `_ for details. ## ## See also: ## * `getLastModificationTime proc`_ ## * `getLastAccessTime proc`_ ## * `fileNewer proc`_ when defined(posix): var res: 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 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 ## `_. ## ## **Examples:** ## ## .. code-block:: ## discard execShellCmd("ls -la") result = exitStatusLikeShell(c_system(command)) 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. 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 getCurrentCompilerExe*(): string {.compileTime.} = discard ## This is `getAppFilename()`_ at compile time. ## ## 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). 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 inclFilePermissions*(filename: string, permissions: set[FilePermission]) {. rtl, extern: "nos$1", tags: [ReadDirEffect, WriteDirEffect], noWeirdTarget.} = ## A convenience proc for: ## ## .. code-block:: 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: ## ## .. code-block:: 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 #include """.} 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) 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.. 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): 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: "".} proc getExecPath2(c: cstring, size: var cuint32): bool {. importc: "_NSGetExecutablePath", header: "".} 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: "".} 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 = "" proc getAppFilename*(): string {.rtl, extern: "nos$1", tags: [ReadIOEffect], noWeirdTarget.} = ## Returns the filename of the application's executable. ## This proc will resolve symlinks. ## ## See also: ## * `getAppDir proc`_ ## * `getCurrentCompilerExe proc`_ # Linux: /proc//exe # Solaris: # /proc//object/a.out (filename only) # /proc//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: result = result.expandFilename 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): raiseOSError(OSErrorCode(-1), "POSIX command line not supported") elif defined(freebsd) or defined(dragonfly) or defined(netbsd): result = getApplFreebsd() elif defined(haiku): result = getApplHaiku() elif defined(openbsd): result = getApplOpenBsd() elif defined(nintendoswitch): result = "" # little heuristic that may work on other POSIX-like systems: if result.len == 0: result = getApplHeuristic() 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 sleep*(milsecs: int) {.rtl, extern: "nos$1", tags: [TimeEffect], noWeirdTarget.} = ## Sleeps `milsecs` milliseconds. when defined(windows): winlean.sleep(int32(milsecs)) else: var a, b: 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 if stat(file, rawInfo) < 0'i32: raiseOSError(osLastError(), file) rawInfo.st_size when defined(windows) or weirdTarget: type DeviceId* = int32 FileId* = int64 else: type DeviceId* = Dev FileId* = Ino 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(a, b): untyped = int64( (uint64(cast[uint32](a))) or (uint64(cast[uint32](b)) shl 32) ) formalInfo.id.device = rawInfo.dwVolumeSerialNumber formalInfo.id.file = merge(rawInfo.nFileIndexLow, rawInfo.nFileIndexHigh) formalInfo.size = merge(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)) when defined(js): when not declared(FileHandle): type FileHandle = distinct int32 when not declared(File): type File = object 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 when defined(windows): var rawInfo: BY_HANDLE_FILE_INFORMATION # We have to use the super special '_get_osfhandle' call (wrapped above) # To transform the C file descriptor to a native file handle. var realHandle = get_osfhandle(handle) if getFileInformationByHandle(realHandle, addr rawInfo) == 0: raiseOSError(osLastError(), $handle) rawToFormalFileInfo(rawInfo, "", result) else: var rawInfo: 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`_ 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 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`_ var a, b: 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 isHidden*(path: string): bool {.noWeirdTarget.} = ## Determines whether ``path`` is hidden or not, using `this ## reference `_. ## ## 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 != ".." proc getCurrentProcessId*(): int {.noWeirdTarget.} = ## Return current process ID. ## ## See also: ## * `osproc.processID(p: 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) 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)