## Copyright (c) 2020-2022 Status Research & Development GmbH ## Licensed under either of ## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) ## * MIT license ([LICENSE-MIT](LICENSE-MIT)) ## at your option. ## This file may not be copied, modified, or distributed except according to ## those terms. ## This module implements number cross-platform IO and OS procedures which do ## not use exceptions and using Result[T] for error handling. ## when (NimMajor, NimMinor) < (1, 4): {.push raises: [Defect].} else: {.push raises: [].} import algorithm import results export results when defined(windows): from strutils import replace, find const GENERIC_READ = 0x80000000'u32 GENERIC_WRITE = 0x40000000'u32 CREATE_NEW = 1'u32 CREATE_ALWAYS = 2'u32 OPEN_EXISTING = 3'u32 OPEN_ALWAYS = 4'u32 TRUNCATE_EXISTING = 5'u32 FILE_FLAG_OVERLAPPED = 0x40000000'u32 FILE_SHARE_READ = 0x00000001'u32 FILE_SHARE_WRITE = 0x00000002'u32 FILE_FLAG_NO_BUFFERING = 0x20000000'u32 FILE_ATTRIBUTE_READONLY = 0x00000001'u32 FILE_ATTRIBUTE_DIRECTORY = 0x00000010'u32 INVALID_HANDLE_VALUE = cast[uint](-1) INVALID_FILE_SIZE = cast[uint32](-1) INVALID_FILE_ATTRIBUTES = cast[uint32](-1) MAX_PATH = 260 ERROR_ALREADY_EXISTS = 183'u32 ERROR_FILE_NOT_FOUND = 2'u32 FILE_BEGIN = 0'u32 FILE_CURRENT = 1'u32 FILE_END = 2'u32 DirSep* = '\\' AltSep* = '/' BothSeps* = {DirSep, AltSep} FileBasicInfoClass = 0'u32 type IoErrorCode* = distinct uint32 IoHandle* = distinct uint SECURITY_ATTRIBUTES {.final, pure.} = object nLength: uint32 lpSecurityDescriptor: pointer bInheritHandle: int32 FILETIME {.final, pure.} = object dwLowDateTime: uint32 dwHighDateTime: uint32 WIN32_FIND_DATAW {.final, pure.} = object dwFileAttributes: uint32 ftCreationTime: FILETIME ftLastAccessTime: FILETIME ftLastWriteTime: FILETIME nFileSizeHigh: uint32 nFileSizeLow: uint32 dwReserved0: uint32 dwReserved1: uint32 cFileName: array[MAX_PATH, Utf16Char] cAlternateFileName: array[14, Utf16Char] BY_HANDLE_FILE_INFORMATION {.final, pure.} = object dwFileAttributes: uint32 ftCreationTime: FILETIME ftLastAccessTime: FILETIME ftLastWriteTime: FILETIME dwVolumeSerialNumber: uint32 nFileSizeHigh: uint32 nFileSizeLow: uint32 nNumberOfLinks: uint32 nFileIndexHigh: uint32 nFileIndexLow: uint32 FILE_BASIC_INFO {.final, pure.} = object creationTime: uint64 lastAccessTime: uint64 lastWriteTime: uint64 changeTime: uint64 fileAttributes: uint32 OVERLAPPED* {.pure, inheritable.} = object internal*: uint internalHigh*: uint offset*: uint32 offsetHigh*: uint32 hEvent*: IoHandle proc getLastError(): uint32 {. importc: "GetLastError", stdcall, dynlib: "kernel32", sideEffect.} proc createDirectoryW(pathName: WideCString, security: var SECURITY_ATTRIBUTES): int32 {. importc: "CreateDirectoryW", dynlib: "kernel32", stdcall, sideEffect.} proc removeDirectoryW(pathName: WideCString): int32 {. importc: "RemoveDirectoryW", dynlib: "kernel32", stdcall, sideEffect.} proc createFileW(fileName: WideCString, dwDesiredAccess: uint32, dwShareMode: uint32, security: var SECURITY_ATTRIBUTES, dwCreationDisposition: uint32, dwFlagsAndAttributes: uint32, hTemplateFile: uint): uint {. importc: "CreateFileW", dynlib: "kernel32", stdcall, sideEffect.} proc deleteFileW(pathName: WideCString): uint32 {. importc: "DeleteFileW", dynlib: "kernel32", stdcall.} proc closeHandle(hobj: uint): int32 {. importc: "CloseHandle", dynlib: "kernel32", stdcall, sideEffect.} proc writeFile(hFile: uint, lpBuffer: pointer, nNumberOfBytesToWrite: uint32, lpNumberOfBytesWritten: var uint32, lpOverlapped: pointer): int32 {. importc: "WriteFile", dynlib: "kernel32", stdcall, sideEffect.} proc readFile(hFile: uint, lpBuffer: pointer, nNumberOfBytesToRead: uint32, lpNumberOfBytesRead: var uint32, lpOverlapped: pointer): int32 {. importc: "ReadFile", dynlib: "kernel32", stdcall, sideEffect.} proc getFileAttributes(path: WideCString): uint32 {. importc: "GetFileAttributesW", dynlib: "kernel32", stdcall, sideEffect.} proc setFileAttributes(path: WideCString, dwAttributes: uint32): uint32 {. importc: "SetFileAttributesW", dynlib: "kernel32", stdcall, sideEffect.} proc getCurrentDirectoryW(nBufferLength: uint32, lpBuffer: WideCString): uint32 {. importc: "GetCurrentDirectoryW", dynlib: "kernel32", stdcall, sideEffect.} proc formatMessageW(dwFlags: uint32, lpSource: pointer, dwMessageId, dwLanguageId: uint32, lpBuffer: pointer, nSize: uint32, arguments: pointer): uint32 {. importc: "FormatMessageW", stdcall, dynlib: "kernel32".} proc localFree(p: pointer): uint {. importc: "LocalFree", stdcall, dynlib: "kernel32", sideEffect.} proc getLongPathNameW(lpszShortPath: WideCString, lpszLongPath: WideCString, cchBuffer: uint32): uint32 {. importc: "GetLongPathNameW", dynlib: "kernel32.dll", stdcall, sideEffect.} proc findFirstFileW(lpFileName: WideCString, lpFindFileData: var WIN32_FIND_DATAW): uint {. importc: "FindFirstFileW", dynlib: "kernel32", stdcall, sideEffect.} proc findClose(hFindFile: uint): int32 {. importc: "FindClose", dynlib: "kernel32", stdcall, sideEffect.} proc getFileInformationByHandle(hFile: uint, info: var BY_HANDLE_FILE_INFORMATION): int32 {. importc: "GetFileInformationByHandle", dynlib: "kernel32", stdcall, sideEffect.} proc getFileInformationByHandleEx(hFile: uint, information: uint32, lpFileInformation: pointer, dwBufferSize: uint32): int32 {. importc: "GetFileInformationByHandleEx", dynlib: "kernel32", stdcall, sideEffect.} proc setFileInformationByHandle(hFile: uint, information: uint32, lpFileInformation: pointer, dwBufferSize: uint32): int32 {. importc: "SetFileInformationByHandle", dynlib: "kernel32", stdcall, sideEffect.} proc getFileSize(hFile: uint, lpFileSizeHigh: var uint32): uint32 {. importc: "GetFileSize", dynlib: "kernel32", stdcall, sideEffect.} proc setFilePointerEx(hFile: uint, liDistanceToMove: int64, lpNewFilePointer: ptr int64, dwMoveMethod: uint32): int32 {. importc: "SetFilePointerEx", dynlib: "kernel32", stdcall, sideEffect.} proc lockFileEx(hFile: uint, dwFlags, dwReserved: uint32, nNumberOfBytesToLockLow, nNumberOfBytesToLockHigh: uint32, lpOverlapped: pointer): uint32 {. importc: "LockFileEx", dynlib: "kernel32", stdcall, sideEffect.} proc unlockFileEx(hFile: uint, dwReserved: uint32, nNumberOfBytesToLockLow, nNumberOfBytesToLockHigh: uint32, lpOverlapped: pointer): uint32 {. importc: "UnlockFileEx", dynlib: "kernel32", stdcall, sideEffect.} const NO_ERROR = IoErrorCode(0) LOCKFILE_EXCLUSIVE_LOCK = 0x00000002'u32 LOCKFILE_FAIL_IMMEDIATELY = 0x00000001'u32 proc `==`*(a: IoErrorCode, b: uint32): bool {.inline.} = (uint32(a) == b) elif defined(posix): import posix const DirSep* = '/' AltSep* = '/' BothSeps* = {'/'} LOCK_SH* = 0x01 LOCK_EX* = 0x02 LOCK_NB* = 0x04 LOCK_UN* = 0x08 type IoHandle* = distinct cint IoErrorCode* = distinct cint when defined(linux): const O_DIRECT = cint(0x4000) O_CLOEXEC = cint(0x2000000) elif defined(freebsd): const O_DIRECT = cint(0x10000) O_CLOEXEC = cint(0x100000) elif defined(dragonflybsd): const O_DIRECT = cint(0x10000) O_CLOEXEC = cint(0x20000) elif defined(netbsd): const O_DIRECT = cint(0x80000) O_CLOEXEC = cint(0x400000) elif defined(openbsd): const O_CLOEXEC = cint(0x10000) elif defined(macosx): const O_CLOEXEC = cint(0x1000000) F_NOCACHE = cint(48) type FlockStruct* {.importc: "struct flock", final, pure, header: "".} = object ltype* {.importc: "l_type".}: cshort lwhence* {.importc: "l_whence".}: cshort start* {.importc: "l_start".}: int length* {.importc: "l_len".}: int pid* {.importc: "l_pid".}: int32 var errno {.importc, header: "".}: cint proc write(a1: cint, a2: pointer, a3: csize_t): int {. importc, header: "", sideEffect.} proc read(a1: cint, a2: pointer, a3: csize_t): int {. importc, header: "", sideEffect.} proc c_strerror(errnum: cint): cstring {. importc: "strerror", header: "", sideEffect.} proc c_free(p: pointer) {. importc: "free", header: "", sideEffect.} proc getcwd(a1: cstring, a2: int): cstring {. importc, header: "", sideEffect.} proc `==`*(a: IoErrorCode, b: cint): bool {.inline.} = (cint(a) == b) type IoResult*[T] = Result[T, IoErrorCode] OpenFlags* {.pure.} = enum Read, Write, Create, Exclusive, Append, Truncate, Inherit, NonBlock, Direct, ShareRead, ShareWrite Permission* = enum UserRead, UserWrite, UserExec, GroupRead, GroupWrite, GroupExec, OtherRead, OtherWrite, OtherExec Permissions* = set[Permission] SeekPosition* = enum SeekBegin, SeekCurrent, SeekEnd AccessFlags* {.pure.} = enum Find, Read, Write, Execute LockType* {.pure.} = enum Shared, Exclusive IoLockHandle* = object handle*: IoHandle offset*: int64 size*: int64 const NimErrorCode = 100_000 UnsupportedFileSize* = IoErrorCode(NimErrorCode) UserErrorCode* = 1_000_000 proc `==`*(a, b: IoErrorCode): bool {.borrow.} proc `$`*(a: IoErrorCode): string {.borrow.} {.push stackTrace:off.} proc ioLastError*(): IoErrorCode {.sideEffect.} = ## Retrieves the last operating system error code. ## ## **Warning**: ## The behaviour of this procedure varies between Windows and POSIX systems. ## On Windows some OS calls can reset the error code to ``0`` causing this ## procedure to return ``0``. It is therefore advised to call this procedure ## immediately after an OS call fails. On POSIX systems this is not a problem. when defined(nimscript): discard elif defined(windows): IoErrorCode(getLastError()) else: IoErrorCode(errno) {.pop.} proc ioErrorMsg*(code: IoErrorCode): string = ## Converts an OS error code into a human readable string. if int(code) == 0: "" elif int(code) >= NimErrorCode: case code of UnsupportedFileSize: "(" & $code & ") " & "File size is unsupported" else: "(" & $code & ") " & "Unknown error" else: when defined(posix): $c_strerror(cint(code)) elif defined(windows): var msgbuf: WideCString if formatMessageW(0x00000100'u32 or 0x00001000'u32 or 0x00000200'u32, nil, uint32(code), 0, addr(msgbuf), 0, nil) != 0'u32: var res = $msgbuf if not(isNil(msgbuf)): discard localFree(cast[pointer](msgbuf)) res else: "" proc normPathEnd(path: var string, trailingSep: bool) = ## Ensures ``path`` has exactly 0 or 1 trailing `DirSep`, depending on ## ``trailingSep``, and taking care of edge cases: it preservers whether ## a path is absolute or relative, and makes sure trailing sep is `DirSep`, ## not `AltSep`. Trailing `/.` are compressed. var i = len(path) if i > 0: while i >= 1: if path[i - 1] in BothSeps: dec(i) elif path[i - 1] == '.' and (i >= 2) and (path[i - 2] in BothSeps): dec(i) else: break if trailingSep: path.setLen(i) path.add DirSep elif i > 0: path.setLen(i) else: path = $DirSep proc splitDrive*(path: string): tuple[head: string, tail: string] = ## Split the pathname ``path`` into drive/UNC sharepoint and relative path ## specifiers. ## ## Returns a 2-tuple (head, tail); either part may be empty. ## ## If the path contained a drive letter, ``head`` will contain everything ## up to and including the colon. e.g. ``splitDrive("c:/dir")`` returns ## ("c:", "/dir"). ## ## If the path contained a UNC path, the ``head`` will contain the host name ## and share up to but not including the fourth directory separator ## character. e.g. ``splitDrive("//host/computer/dir")`` returns ## ("//host/computer", "/dir") ## ## Note, paths cannot contain both a drive letter and a UNC path. when defined(posix): # On Posix, drive is always empty ("", path) elif defined(windows): if len(path) < 2: return ("", path) let normp = path.replace('/', '\\') if (len(path) > 2) and normp[0] == '\\' and normp[1] == '\\' and normp[2] != '\\': let index = normp.find('\\', 2) if index == -1: return ("", path) let index2 = normp.find('\\', index + 1) if index2 == index + 1: return ("", path) return (path[0 ..< index2], path[index2 .. ^1]) if normp[1] == ':': return (path[0 .. 1], path[2 .. ^1]) return ("", path) proc splitPath*(path: string): tuple[head: string, tail: string] = ## Split the pathname ``path`` into a pair, (head, tail) where tail is the ## last pathname component and head is everything leading up to that. ## ## * The tail part will never contain a slash. ## * If path ends in a slash, tail will be empty. ## * If there is no slash in path, head will be empty. ## * If path is empty, both head and tail are empty. ## * Trailing slashes are stripped from head unless it is the root ## (one or more slashes only) if len(path) == 0: ("", "") else: let (drive, p) = splitDrive(path) let pathlen = len(p) var i = pathlen while (i != 0) and (p[i - 1]) notin BothSeps: dec(i) let head = p[0 ..< i] let tail = p[i ..< pathlen] var headStrip = head i = len(headStrip) while (i != 0) and (headStrip[i - 1]) in BothSeps: dec(i) headStrip.setLen(i) if len(headStrip) == 0: (drive & head, tail) else: (drive & headStrip, tail) proc basename*(path: string): string = ## Return the base name of pathname ``path``. ## ## Note that the result of this procedure is different from the Unix basename ## program; where basename for "/foo/bar/" returns "bar", the basename() ## procedure returns an empty string (""). splitPath(path)[1] proc dirname*(path: string): string = ## Return the directory name of pathname ``path``. splitPath(path)[0] when defined(windows): proc toLongPath*(path: string): IoResult[string] = let shortPath = newWideCString(path) var buffer = newSeq[Utf16Char](len(path) * 2 + 1) while true: let res = getLongPathNameW(shortPath, cast[WideCString](addr buffer[0]), uint32(len(buffer))) if res == 0: return err(ioLastError()) else: if res <= uint32(len(buffer)): return ok($cast[WideCString](addr buffer[0])) else: buffer.setLen(res) continue proc getCurrentDir*(): IoResult[string] = ## Returns string containing an absolute pathname that is the current working ## directory of the calling process. when defined(posix): while true: let res = getcwd(nil, 0) if isNil(res): let errCode = ioLastError() if errCode == EINTR: continue else: return err(errCode) else: var buffer = $res c_free(res) return ok(buffer) elif defined(windows): var bufsize = uint32(MAX_PATH) var buffer = newWideCString("", int(bufsize)) while true: let res = getCurrentDirectoryW(bufsize, buffer) if res == 0'u32: return err(ioLastError()) elif res > bufsize: buffer = newWideCString("", int(res)) bufsize = res else: return ok(buffer$int(res)) proc setUmask*(mask: int): int {.inline.} = ## Procedure shall set the file mode creation mask of the process to ``mask`` ## and return the previous value of the ``mask``. ## ## Note: On Windows this is empty procedure which always returns ``0``. when defined(windows): 0 else: int(posix.umask(Mode(mask))) proc rawCreateDir(dir: string, mode: int = 0o755, secDescriptor: pointer = nil): IoResult[bool] = ## Attempts to create a directory named ``dir``. ## ## The argument ``mode`` specifies the mode for the new directory. ## It is modified by the process's umask in the usual way: in the absence of ## a default ACL, the mode of the created directory is ## (mode and not(umask) and 0o777). Whether other mode bits are honored for ## the created directory depends on the operating system. ## ## Returns ``true`` if directory was successfully created and ``false`` if ## path ``dir`` is already exists. when defined(posix): when defined(solaris): let existFlags = {EEXIST, ENOSYS} elif defined(haiku): let existFlags = {EEXIST, EROFS} else: let existFlags = {EEXIST} while true: let omask = setUmask(0) let res = posix.mkdir(cstring(dir), Mode(mode)) discard setUmask(omask) if res == 0'i32: return ok(true) else: let errCode = ioLastError() if cint(errCode) in existFlags: return ok(false) elif errCode == EINTR: continue else: return err(errCode) elif defined(windows): var sa = SECURITY_ATTRIBUTES( nLength: uint32(sizeof(SECURITY_ATTRIBUTES)), lpSecurityDescriptor: secDescriptor, bInheritHandle: 0 ) let res = createDirectoryW(newWideCString(dir), sa) if res != 0'i32: ok(true) else: let errCode = ioLastError() if errCode == ERROR_ALREADY_EXISTS: ok(false) else: err(errCode) proc removeDir*(dir: string): IoResult[void] = ## Deletes a directory, which must be empty. when defined(posix): while true: let res = posix.rmdir(cstring(dir)) if res == 0: return ok() else: let errCode = ioLastError() if errCode == EINTR: continue else: return err(errCode) elif defined(windows): let res = removeDirectoryW(newWideCString(dir)) if res != 0'i32: ok() else: err(ioLastError()) proc removeFile*(path: string): IoResult[void] = ## Deletes a file ``path``. ## ## Procedure will not fail, if file do not exist. when defined(posix): if posix.unlink(path) != 0'i32: let errCode = ioLastError() if errCode == ENOENT: ok() else: err(errCode) else: ok() elif defined(windows): if deleteFileW(newWideCString(path)) == 0: let errCode = ioLastError() if errCode == ERROR_FILE_NOT_FOUND: ok() else: err(errCode) else: ok() proc isFile*(path: string): bool = ## Returns ``true`` if ``path`` exists and is a regular file or symlink. when defined(posix): var a: posix.Stat let res = posix.stat(path, a) if res == -1: false else: posix.S_ISREG(a.st_mode) elif defined(windows): let res = getFileAttributes(newWideCString(path)) if res == INVALID_FILE_ATTRIBUTES: false else: (res and FILE_ATTRIBUTE_DIRECTORY) == 0'u32 proc isDir*(path: string): bool = ## Returns ``true`` if ``path`` exists and is a directory. when defined(posix): var a: posix.Stat let res = posix.stat(path, a) if res == -1: false else: posix.S_ISDIR(a.st_mode) elif defined(windows): let res = getFileAttributes(newWideCString(path)) if res == INVALID_FILE_ATTRIBUTES: false else: (res and FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY proc getPathItems(path: string, reverse: bool): seq[string] = var paths: seq[string] let root = $DirSep when defined(windows): let (drive, dpath) = splitDrive(path) var curpath = dpath else: var curpath = path normPathEnd(curpath, trailingSep = false) while true: let curbase = basename(curpath) let curdir = dirname(curpath) curpath = curdir if len(curbase) > 0: when defined(posix): if len(curdir) > 0 and curdir != root: paths.add(curdir & DirSep & curbase) else: paths.add(curdir & curbase) elif defined(windows): if len(curdir) > 0 and curdir != root: paths.add(drive & curdir & DirSep & curbase) else: paths.add(drive & curdir & curbase) else: break if reverse: paths.reverse() paths proc createPath*(path: string, createMode: int = 0o755, secDescriptor: pointer = nil): IoResult[void] = ## Creates the full path ``path`` with mode ``createMode``. ## ## Path may contain several subfolders that do not exist yet. ## The full path is created. If this fails, error will be returned. ## ## It does **not** fail if the folder already exists because for ## most usages this does not indicate an error. let paths = getPathItems(path, true) for item in paths: let res = rawCreateDir(item, createMode, secDescriptor) if res.isErr(): return err(res.error) ok() proc toPermissions*(mask: int): Permissions = ## Converts permissions mask's integer to set of ``Permission``. var res: Permissions when defined(posix): if (mask and S_IRUSR) != 0: res.incl(UserRead) if (mask and S_IWUSR) != 0: res.incl(UserWrite) if (mask and S_IXUSR) != 0: res.incl(UserExec) if (mask and S_IRGRP) != 0: res.incl(GroupRead) if (mask and S_IWGRP) != 0: res.incl(GroupWrite) if (mask and S_IXGRP) != 0: res.incl(GroupExec) if (mask and S_IROTH) != 0: res.incl(OtherRead) if (mask and S_IWOTH) != 0: res.incl(OtherWrite) if (mask and S_IXOTH) != 0: res.incl(OtherExec) res elif defined(windows): if (mask and 0o400) != 0: res.incl(UserRead) if (mask and 0o200) != 0: res.incl(UserWrite) if (mask and 0o100) != 0: res.incl(UserExec) if (mask and 0o40) != 0: res.incl(GroupRead) if (mask and 0o20) != 0: res.incl(GroupWrite) if (mask and 0o10) != 0: res.incl(GroupExec) if (mask and 0o4) != 0: res.incl(OtherRead) if (mask and 0o2) != 0: res.incl(OtherWrite) if (mask and 0o1) != 0: res.incl(OtherExec) res proc toInt*(mask: Permissions): int = ## Converts set of ``Permission`` to permissions mask's integer. var rnum = 0 when defined(windows): if UserRead in mask: rnum = rnum or 0o400 if UserWrite in mask: rnum = rnum or 0o200 if UserExec in mask: rnum = rnum or 0o100 if GroupRead in mask: rnum = rnum or 0o40 if GroupWrite in mask: rnum = rnum or 0o20 if GroupExec in mask: rnum = rnum or 0o10 if OtherRead in mask: rnum = rnum or 0o4 if OtherWrite in mask: rnum = rnum or 0o2 if OtherExec in mask: rnum = rnum or 0o1 rnum elif defined(posix): if UserRead in mask: rnum = rnum or S_IRUSR if UserWrite in mask: rnum = rnum or S_IWUSR if UserExec in mask: rnum = rnum or S_IXUSR if GroupRead in mask: rnum = rnum or S_IRGRP if GroupWrite in mask: rnum = rnum or S_IWGRP if GroupExec in mask: rnum = rnum or S_IXGRP if OtherRead in mask: rnum = rnum or S_IROTH if OtherWrite in mask: rnum = rnum or S_IWOTH if OtherExec in mask: rnum = rnum or S_IXOTH rnum else: 0o777 proc getPermissions*(pathName: string): IoResult[int] = ## Retreive permissions of file/folder ``pathName`` and return it as integer. when defined(posix): var a: posix.Stat let res = posix.stat(pathName, a) if res == 0: ok(int(a.st_mode) and 0o777) else: err(ioLastError()) elif defined(windows): let res = getFileAttributes(newWideCString(pathName)) if res == INVALID_FILE_ATTRIBUTES: err(ioLastError()) else: if (res and FILE_ATTRIBUTE_READONLY) == FILE_ATTRIBUTE_READONLY: ok(0o555) else: ok(0o777) else: ok(0o777) proc getPermissions*(handle: IoHandle): IoResult[int] = ## Retrieve permissions for file descriptor ``handle`` and return it as ## integer. when defined(posix): var statbuf: posix.Stat let res = posix.fstat(cint(handle), statbuf) if res == 0: ok(int(statbuf.st_mode) and 0o777) else: err(ioLastError()) elif defined(windows): var info: BY_HANDLE_FILE_INFORMATION let res = getFileInformationByHandle(uint(handle), info) if res != 0: let attr = info.dwFileAttributes if (attr and FILE_ATTRIBUTE_READONLY) == FILE_ATTRIBUTE_READONLY: ok(0o555) else: ok(0o777) else: err(ioLastError()) else: ok(0o777) proc getPermissionsSet*(pathName: string): IoResult[Permissions] = ## Retreive permissions of file/folder ``pathName`` and return set of ## ``Permission`. let mask = ? getPermissions(pathName) when defined(windows) or defined(posix): ok(mask.toPermissions()) else: ok({UserRead .. OtherExec}) proc getPermissionsSet*(handle: IoHandle): IoResult[Permissions] = let mask = ? getPermissions(handle) when defined(windows) or defined(posix): ok(mask.toPermissions()) else: ok({UserRead .. OtherExec}) proc setPermissions*(pathName: string, mask: int): IoResult[void] = ## Set permissions for file/folder ``pathame``. when defined(windows): let gres = getFileAttributes(newWideCString(pathName)) if gres == INVALID_FILE_ATTRIBUTES: err(ioLastError()) else: let nmask = if (mask and 0o222) == 0: gres or uint32(FILE_ATTRIBUTE_READONLY) else: gres and not(FILE_ATTRIBUTE_READONLY) let sres = setFileAttributes(newWideCString(pathName), nmask) if sres == 0: err(ioLastError()) else: ok() elif defined(posix): while true: let omask = setUmask(0) let res = posix.chmod(pathName, Mode(mask)) discard setUmask(omask) if res == 0: return ok() else: let errCode = ioLastError() if errCode == EINTR: continue else: return err(errCode) proc setPermissions*(handle: IoHandle, mask: int): IoResult[void] = ## Set permissions for handle ``handle``. when defined(posix): while true: let omask = setUmask(0) let res = posix.fchmod(cint(handle), Mode(mask)) discard setUmask(omask) if res == 0: return ok() else: let errCode = ioLastError() if errCode == EINTR: continue else: return err(errCode) elif defined(windows): var info: FILE_BASIC_INFO let infoSize = uint32(sizeof(FILE_BASIC_INFO)) let gres = getFileInformationByHandleEx(uint(handle), FileBasicInfoClass, cast[pointer](addr info), infoSize) if gres == 0: err(ioLastError()) else: info.fileAttributes = if (mask and 0o222) == 0: info.fileAttributes or uint32(FILE_ATTRIBUTE_READONLY) else: info.fileAttributes and not(FILE_ATTRIBUTE_READONLY) let sres = setFileInformationByHandle(uint(handle), FileBasicInfoClass, cast[pointer](addr info), infoSize) if sres == 0: err(ioLastError()) else: ok() proc setPermissions*(pathName: string, mask: Permissions): IoResult[void] = ## Set permissions for file/folder ``pathame`` using mask ``mask``. setPermissions(pathName, mask.toInt()) proc setPermissions*(handle: IoHandle, mask: Permissions): IoResult[void] = ## Set permissions for file descriptor ``handle`` using mask ``mask``. setPermissions(handle, mask.toInt()) proc fileAccessible*(pathName: string, mask: set[AccessFlags]): bool = ## Checks the file ``pathName`` for accessibility according to the bit ## pattern contained in ``mask``. when defined(posix): var mode: cint if AccessFlags.Find in mask: mode = mode or posix.F_OK if AccessFlags.Read in mask: mode = mode or posix.R_OK if AccessFlags.Write in mask: mode = mode or posix.W_OK if AccessFlags.Execute in mask: mode = mode or posix.X_OK let res = posix.access(cstring(pathName), mode) if res == 0: true else: false elif defined(windows): let res = getFileAttributes(newWideCString(pathName)) if res == INVALID_FILE_ATTRIBUTES: return false if AccessFlags.Write in mask: if (res and FILE_ATTRIBUTE_READONLY) == FILE_ATTRIBUTE_READONLY: return false else: return true return true proc toString*(mask: Permissions): string = ## Return mask representation as human-readable string in format ## "0xxx (---------)" where `xxx` is numeric representation of permissions. var rnum = 0 var rstr = "0000 (---------)" if UserRead in mask: rstr[6] = 'r' rnum = rnum or 0o400 if UserWrite in mask: rstr[7] = 'w' rnum = rnum or 0o200 if UserExec in mask: rstr[8] = 'x' rnum = rnum or 0o100 if GroupRead in mask: rstr[9] = 'r' rnum = rnum or 0o40 if GroupWrite in mask: rstr[10] = 'w' rnum = rnum or 0o20 if GroupExec in mask: rstr[11] = 'x' rnum = rnum or 0o10 if OtherRead in mask: rstr[12] = 'r' rnum = rnum or 0o4 if OtherWrite in mask: rstr[13] = 'w' rnum = rnum or 0o2 if OtherExec in mask: rstr[14] = 'x' rnum = rnum or 0o1 if (rnum and 0o700) != 0: rstr[1] = ($((rnum shr 6) and 0x07))[0] if (rnum and 0o70) != 0: rstr[2] = ($((rnum shr 3) and 0x07))[0] if (rnum and 0o7) != 0: rstr[3] = ($(rnum and 0x07))[0] rstr proc checkPermissions*(pathName: string, mask: int): bool = ## Checks if the file ``pathName`` permissions is equal to ``mask``. when defined(windows): true elif defined(posix): var statbuf: posix.Stat let res = posix.stat(pathName, statbuf) if res == 0: (int(statbuf.st_mode) and 0o777) == mask else: false else: true proc openFile*(pathName: string, flags: set[OpenFlags], createMode: int = 0o644, secDescriptor: pointer = nil): IoResult[IoHandle] = when defined(posix): var cflags: cint if (OpenFlags.Read in flags) and (OpenFlags.Write in flags): cflags = cflags or posix.O_RDWR else: if OpenFlags.Write in flags: cflags = cflags or posix.O_WRONLY else: cflags = cflags or posix.O_RDONLY if OpenFlags.Create in flags: cflags = cflags or posix.O_CREAT if OpenFlags.Exclusive in flags: cflags = cflags or posix.O_EXCL if OpenFlags.Truncate in flags: cflags = cflags or posix.O_TRUNC if OpenFlags.Append in flags: cflags = cflags or posix.O_APPEND when defined(linux) or defined(freebsd) or defined(netbsd) or defined(dragonflybsd): if OpenFlags.Direct in flags: cflags = cflags or O_DIRECT if OpenFlags.Inherit notin flags: cflags = cflags or O_CLOEXEC if OpenFlags.NonBlock in flags: cflags = cflags or posix.O_NONBLOCK while true: let omask = setUmask(0) let ores = posix.open(cstring(pathName), cflags, Mode(createMode)) discard setUmask(omask) if ores == -1: let errCode = ioLastError() if errCode == EINTR: continue else: return err(errCode) else: when defined(macosx): if OpenFlags.Direct in flags: while true: let fres = posix.fcntl(cint(ores), F_NOCACHE, 1) if fres == -1: let errCode = ioLastError() if errCode == EINTR: continue else: return err(errCode) else: return ok(IoHandle(ores)) else: return ok(IoHandle(ores)) else: return ok(IoHandle(ores)) elif defined(windows): var dwAccess: uint32 dwShareMode: uint32 dwCreation: uint32 dwFlags: uint32 var sa = SECURITY_ATTRIBUTES( nLength: uint32(sizeof(SECURITY_ATTRIBUTES)), lpSecurityDescriptor: secDescriptor, bInheritHandle: 0 ) if (OpenFlags.Write in flags) and (OpenFlags.Read in flags): dwAccess = dwAccess or (GENERIC_READ or GENERIC_WRITE) else: if OpenFlags.Write in flags: dwAccess = dwAccess or GENERIC_WRITE else: dwAccess = dwAccess or GENERIC_READ if {OpenFlags.Create, OpenFlags.Exclusive} <= flags: dwCreation = dwCreation or CREATE_NEW elif OpenFlags.Truncate in flags: if OpenFlags.Create in flags: dwCreation = dwCreation or CREATE_ALWAYS elif OpenFlags.Read notin flags: dwCreation = dwCreation or TRUNCATE_EXISTING elif OpenFlags.Append in flags: dwCreation = dwCreation or OPEN_EXISTING elif OpenFlags.Create in flags: dwCreation = dwCreation or OPEN_ALWAYS else: dwCreation = dwCreation or OPEN_EXISTING if dwCreation == OPEN_EXISTING and ((dwAccess and (GENERIC_READ or GENERIC_WRITE)) == GENERIC_READ): dwShareMode = dwShareMode or FILE_SHARE_READ if OpenFlags.ShareRead in flags: dwShareMode = dwShareMode or FILE_SHARE_READ if OpenFlags.ShareWrite in flags: dwShareMode = dwShareMode or FILE_SHARE_WRITE if OpenFlags.NonBlock in flags: dwFlags = dwFlags or FILE_FLAG_OVERLAPPED if OpenFlags.Direct in flags: dwFlags = dwFlags or FILE_FLAG_NO_BUFFERING if OpenFlags.Inherit in flags: sa.bInheritHandle = 1 let res = createFileW(newWideCString(pathName), dwAccess, dwShareMode, sa, dwCreation, dwFlags, 0'u32) if res == INVALID_HANDLE_VALUE: err(ioLastError()) else: ok(IoHandle(res)) proc closeFile*(handle: IoHandle): IoResult[void] = ## Closes file descriptor handle ``handle``. when defined(windows): let res = closeHandle(uint(handle)) if res == 0: err(ioLastError()) else: ok() elif defined(posix): let res = posix.close(cint(handle)) if res == -1: err(ioLastError()) else: ok() proc writeFile*(handle: IoHandle, data: openArray[byte]): IoResult[uint] = ## Write ``data`` bytes to file descriptor ``handle``. ## ## Returns number of bytes written. when defined(posix): if len(data) > 0: while true: let res = write(cint(handle), unsafeAddr data[0], csize_t(len(data))) if res == -1: let errCode = ioLastError() if errCode == EINTR: continue else: return err(errCode) else: return ok(uint(res)) else: return ok(0) elif defined(windows): if len(data) > 0: var lpNumberOfBytesWritten = 0'u32 let res = writeFile(uint32(handle), unsafeAddr data[0], uint32(len(data)), lpNumberOfBytesWritten, nil) if res != 0: ok(lpNumberOfBytesWritten) else: err(ioLastError()) else: ok(0) proc writeFile*(handle: IoHandle, data: openArray[char]): IoResult[uint] {.inline.} = ## Write ``data`` characters to file descriptor ``handle``. ## ## Returns number of characters written. writeFile(handle, data.toOpenArrayByte(0, len(data) - 1)) proc readFile*(handle: IoHandle, data: var openArray[byte]): IoResult[uint] = ## Reads ``len(data)`` bytes from file descriptor ``handle`` and store this ## bytes to ``data``. ## ## Returns number of bytes read from file descriptor. when defined(posix): if len(data) > 0: while true: let res = read(cint(handle), unsafeAddr data[0], csize_t(len(data))) if res == -1: let errCode = ioLastError() if errCode == EINTR: continue else: return err(errCode) else: return ok(uint(res)) else: return ok(0) elif defined(windows): if len(data) > 0: var lpNumberOfBytesRead = 0'u32 let res = readFile(uint32(handle), unsafeAddr data[0], uint32(len(data)), lpNumberOfBytesRead, nil) if res != 0: ok(lpNumberOfBytesRead) else: err(ioLastError()) else: ok(0) proc readFile*(handle: IoHandle, data: var openArray[char]): IoResult[uint] {.inline.} = ## Reads ``len(data)`` characters from file descriptor ``handle`` and store ## this characters to ``data``. ## ## Returns number of bytes characters read from file descriptor. readFile(handle, data.toOpenArrayByte(0, len(data) - 1)) proc writeFile*(pathName: string, data: openArray[byte], createMode: int = 0o644, secDescriptor: pointer = nil): IoResult[void] = ## Opens a file named ``pathName`` for writing. Then writes the ## content ``data`` completely to the file and closes the file afterwards. ## ## If file is not exists it will be created with permissions mask ## ``createMode`` (default value is 0o644). ## ## If file is already exists, it will be truncated to 0 size first, ## after it will try to set permissions to ``createMode`` and only ## after success it will write data ``data`` to file. let flags = {OpenFlags.Write, OpenFlags.Truncate, OpenFlags.Create} let handle = ? openFile(pathName, flags, createMode, secDescriptor) ? setPermissions(handle, createMode) var offset = 0 while offset < len(data): let res = writeFile(handle, data.toOpenArray(offset, len(data) - 1)) if res.isErr(): # Do not care about `closeFile(handle)` error because we already in # error handler. discard closeFile(handle) return err(res.error) else: offset = offset + int(res.get()) ? closeFile(handle) ok() when defined(windows): template makeInt64(a, b: uint32): int64 = (int64(a and 0x7FFF_FFFF'u32) shl 32) or int64(b and 0xFFFF_FFFF'u32) template makeUint32(a: uint64): tuple[lowPart: uint32, highPart: uint32] = (uint32(a and 0xFFFF_FFFF'u64), uint32((a shr 32) and 0xFFFF_FFFF'u64)) proc writeFile*(pathName: string, data: openArray[char], createMode: int = 0o644, secDescriptor: pointer = nil): IoResult[void] {.inline.} = ## Opens a file named ``pathName`` for writing. Then writes the ## content ``data`` completely to the file and closes the file afterwards. ## ## If file is not exists it will be created with permissions mask ## ``createMode`` (default value is 0o644). writeFile(pathName, data.toOpenArrayByte(0, len(data) - 1), createMode, secDescriptor) proc getFileSize*(pathName: string): IoResult[int64] = ## Returns size in bytes of the specified file ``pathName``. when defined(posix): var a: posix.Stat let res = posix.stat(pathName, a) if res == -1: err(ioLastError()) else: ok(int64(a.st_size)) elif defined(windows): var wfd: WIN32_FIND_DATAW let res = findFirstFileW(newWideCString(pathName), wfd) if res == INVALID_HANDLE_VALUE: err(ioLastError()) else: if findClose(res) == 0: err(ioLastError()) else: ok(makeInt64(wfd.nFileSizeHigh, wfd.nFileSizeLow)) proc getFileSize*(handle: IoHandle): IoResult[int64] = ## Returns size in bytes of file specified by file descriptor ``handle``. when defined(posix): var statbuf: posix.Stat let res = posix.fstat(cint(handle), statbuf) if res == 0: ok(int64(statbuf.st_size)) else: err(ioLastError()) elif defined(windows): var highPart: uint32 let res = getFileSize(uint(handle), highPart) if res == INVALID_FILE_SIZE: let errCode = ioLastError() if errCode == NO_ERROR: ok(makeInt64(highPart, res)) else: err(errCode) else: ok(makeInt64(highPart, res)) proc getFilePos*(handle: IoHandle): IoResult[int64] = ## Returns current file offset for the open file associated with the file ## descriptor ``handle``. when defined(windows): let whence = FILE_CURRENT var pos: int64 let res = setFilePointerEx(uint(handle), 0'i64, addr pos, whence) if res == 0: err(ioLastError()) else: ok(pos) elif defined(posix): let res = int64(posix.lseek(cint(handle), Off(0), posix.SEEK_CUR)) if res == -1'i64: err(ioLastError()) else: ok(int64(res)) proc setFilePos*(handle: IoHandle, offset: int64, whence: SeekPosition): IoResult[void] = ## Procedure shall set the file offset for the open file associated with the ## file descriptor ``handle``, as follows: ## * If whence is ``SeekPosition.SeekBegin``, the file offset shall be set ## to ``offset`` bytes. ## * If whence is ``SeekPosition.SeekCur``, the file offset shall be set to ## its current location plus ``offset``. ## * If whence is ``SeekPosition.SeekEnd``, the file offset shall be set to ## the size of the file plus ``offset``. when defined(windows): let pos = case whence of SeekBegin: FILE_BEGIN of SeekCurrent: FILE_CURRENT of SeekEnd: FILE_END let res = setFilePointerEx(uint(handle), offset, nil, pos) if res == 0: err(ioLastError()) else: ok() else: let pos = case whence of SeekBegin: posix.SEEK_SET of SeekCurrent: posix.SEEK_CUR of SeekEnd: posix.SEEK_END let res = int64(posix.lseek(cint(handle), Off(offset), pos)) if res == -1'i64: err(ioLastError()) else: ok() proc checkFileSize*(value: int64): IoResult[void] = ## Checks if ``value`` fits into supported by Nim string/sequence indexing ## mechanism. ## ## * For 32-bit systems the maximum value is 0x7FFF_FFFF'i64. ## * For 64-bit systems the maximum value is 0x7FFF_FFFF_FFFF_FFFF'i64. when sizeof(int) == 4: if value > 0x7FFF_FFFF'i64: err(UnsupportedFileSize) else: ok() elif sizeof(int) == 8: ok() proc readFile*[T: byte|char](pathName: string, data: var openArray[T]): IoResult[uint] = ## Try to read all data from file ``pathName`` and store it to ``data``. ## If size of ``data`` is not enough to store all data, only part of data ## will be stored. ## ## Returns number of bytes read. let flags = {OpenFlags.Read} let handle = ? openFile(pathName, flags) let res = readFile(handle, data) if res.isErr(): # Do not care about `closeFile(handle)` error because we already in # error handler. discard closeFile(handle) err(res.error) else: ? closeFile(handle) ok(res.get()) proc readFile*[T: seq[byte]|string](pathName: string, data: var T): IoResult[void] = ## Read all data from file ``pathName`` and store it to ``data``. let fileSize = ? getFileSize(pathName) ? checkFileSize(fileSize) data.setLen(fileSize) let res {.used.} = ? readFile(pathName, data.toOpenArray(0, len(data) - 1)) ok() proc readAllBytes*(pathName: string): IoResult[seq[byte]] = ## Read all bytes/characters from file and return it as sequence of bytes. var data: seq[byte] ? readFile(pathName, data) ok(data) proc readAllChars*(pathName: string): IoResult[string] = ## Read all bytes/characters from file and return it as string. var data: string ? readFile(pathName, data) ok(data) proc readAllFile*(pathName: string): IoResult[seq[byte]] = ## Alias for ``readAllBytes()``. readAllBytes(pathName) proc lockFile*(handle: IoHandle, kind: LockType, offset, size: int64): IoResult[void] = ## Apply shared or exclusive file segment lock for file handle ``handle`` and ## range specified by ``offset`` and ``size`` parameters. ## ## ``kind`` - type of lock (shared or exclusive). Please note that only ## exclusive locks have cross-platform compatible behavior. Hovewer, exclusive ## locks require ``handle`` to be opened for writing. ## ## ``offset`` - starting byte offset in the file where the lock should ## begin. ``offset`` should be always bigger or equal to ``0``. ## ## ``size`` - length of the byte range to be locked. ``size`` should be always ## bigger or equal to ``0``. ## ## If ``offset`` and ``size`` are both equal to ``0`` the entire file is locked. doAssert(offset >= 0) doAssert(size >= 0) when defined(posix): let ltype = case kind of LockType.Shared: cshort(posix.F_RDLCK) of LockType.Exclusive: cshort(posix.F_WRLCK) var flockObj = when sizeof(int) == 8: # There is no need to perform overflow check, so we just cast. FlockStruct(ltype: ltype, lwhence: cshort(posix.SEEK_SET), start: cast[int](offset), length: cast[int](size)) else: # Currently we do not support `__USE_FILE_OFFSET64` or # `__USE_LARGEFILE64` because its Linux specific #defines, and is not # present on BSD systems. Therefore, on 32bit systems we do not support # range locks which exceed `int32` value size. if offset > int64(high(int)): return err(IoErrorCode(EFBIG)) if size > int64(high(int)): return err(IoErrorCode(EFBIG)) # We already made overflow check, so we just cast. FlockStruct(ltype: ltype, lwhence: cshort(posix.SEEK_SET), start: cast[int](offset), length: cast[int](size)) while true: let res = posix.fcntl(cint(handle), posix.F_SETLK, addr flockObj) if res == -1: let errCode = ioLastError() if errCode == EINTR: continue else: return err(errCode) else: return ok() elif defined(windows): let (lowOffsetPart, highOffsetPart, lowSizePart, highSizePart) = if offset == 0'i64 and size == 0'i64: # We try to keep cross-platform behavior on Windows. And we can do it # because: Locking a region that goes beyond the current end-of-file # position is not an error. (0'u32, 0'u32, 0xFFFF_FFFF'u32, 0xFFFF_FFFF'u32) else: let offsetTuple = makeUint32(uint64(offset)) let sizeTuple = makeUint32(uint64(size)) (offsetTuple[0], offsetTuple[1], sizeTuple[0], sizeTuple[1]) var ovl = OVERLAPPED(offset: lowOffsetPart, offsetHigh: highOffsetPart) let flags = case kind of LockType.Shared: LOCKFILE_FAIL_IMMEDIATELY of LockType.Exclusive: LOCKFILE_FAIL_IMMEDIATELY or LOCKFILE_EXCLUSIVE_LOCK res = lockFileEx(uint(handle), flags, 0'u32, lowSizePart, highSizePart, addr ovl) if res == 0: err(ioLastError()) else: ok() proc unlockFile*(handle: IoHandle, offset, size: int64): IoResult[void] = ## Clear shared or exclusive file segment lock for file handle ``handle`` and ## range specified by ``offset`` and ``size`` parameters. ## ## ``offset`` - starting byte offset in the file where the lock placed. ## ``offset`` should be always bigger or equal to ``0``. ## ## ``size`` - length of the byte range to be unlocked. ``size`` should be ## always bigger or equal to ``0``. doAssert(offset >= 0) doAssert(size >= 0) when defined(posix): let ltype = cshort(posix.F_UNLCK) var flockObj = when sizeof(int) == 8: # There is no need to perform overflow check, so we just cast. FlockStruct(ltype: ltype, lwhence: cshort(posix.SEEK_SET), start: cast[int](offset), length: cast[int](size)) else: # Currently we do not support `__USE_FILE_OFFSET64` because its # Linux specific #define, and it not present in BSD systems. So # on 32bit systems we do not support range locks which exceed `int32` # value size. if offset > int64(high(int)): return err(IoErrorCode(EFBIG)) if size > int64(high(int)): return err(IoErrorCode(EFBIG)) # We already made overflow check, so we just cast. FlockStruct(ltype: ltype, lwhence: cshort(posix.SEEK_SET), start: cast[int](offset), length: cast[int](size)) while true: let res = posix.fcntl(cint(handle), F_SETLK, addr flockObj) if res == -1: let errCode = ioLastError() if errCode == EINTR: continue else: return err(errCode) else: return ok() elif defined(windows): let (lowOffsetPart, highOffsetPart, lowSizePart, highSizePart) = if offset == 0'i64 and size == 0'i64: # We try to keep cross-platform behavior on Windows. And we can do it # because: Locking a region that goes beyond the current end-of-file # position is not an error. (0'u32, 0'u32, 0xFFFF_FFFF'u32, 0xFFFF_FFFF'u32) else: let offsetTuple = makeUint32(uint64(offset)) let sizeTuple = makeUint32(uint64(size)) (offsetTuple[0], offsetTuple[1], sizeTuple[0], sizeTuple[1]) var ovl = OVERLAPPED(offset: lowOffsetPart, offsetHigh: highOffsetPart) let res = unlockFileEx(uint(handle), 0'u32, lowSizePart, highSizePart, addr ovl) if res == 0: err(ioLastError()) else: ok() proc lockFile*(handle: IoHandle, kind: LockType): IoResult[IoLockHandle] = ## Apply exclusive or shared lock to whole file specified by file handle ## ``handle``. ## ## ``kind`` - type of lock (shared or exclusive). Please note that only ## exclusive locks have cross-platform compatible behavior. Hovewer, exclusive ## locks require ``handle`` to be opened for writing. ## ## On success returns ``IoLockHandle`` object which could be used for unlock. ? lockFile(handle, kind, 0'i64, 0'i64) ok(IoLockHandle(handle: handle, offset: 0'i64, size: 0'i64)) proc unlockFile*(lock: IoLockHandle): IoResult[void] = ## Clear shared or exclusive lock ``lock``. let res = unlockFile(lock.handle, lock.offset, lock.size) if res.isErr(): err(res.error()) else: ok()