nim-stew/stew/io2.nim

1315 lines
42 KiB
Nim

## Copyright (c) 2020 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.
##
{.push raises: [Defect].}
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_FLAG_NO_BUFFERING = 0x20000000'u32
FILE_SHARE_READ = 1'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
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".}
proc getLongPathNameW(lpszShortPath: WideCString, lpszLongPath: WideCString,
cchBuffer: uint32): uint32 {.
importc: "GetLongPathNameW", dynlib: "kernel32.dll", stdcall.}
proc findFirstFileW(lpFileName: WideCString,
lpFindFileData: var WIN32_FIND_DATAW): uint {.
importc: "FindFirstFileW", dynlib: "kernel32", stdcall.}
proc findClose(hFindFile: uint): int32 {.
importc: "FindClose", dynlib: "kernel32", stdcall.}
proc getFileInformationByHandle(hFile: uint,
info: var BY_HANDLE_FILE_INFORMATION): int32 {.
importc: "GetFileInformationByHandle", dynlib: "kernel32", stdcall.}
proc getFileInformationByHandleEx(hFile: uint, information: uint32,
lpFileInformation: pointer,
dwBufferSize: uint32): int32 {.
importc: "GetFileInformationByHandleEx", dynlib: "kernel32", stdcall.}
proc setFileInformationByHandle(hFile: uint, information: uint32,
lpFileInformation: pointer,
dwBufferSize: uint32): int32 {.
importc: "SetFileInformationByHandle", dynlib: "kernel32", stdcall.}
proc getFileSize(hFile: uint, lpFileSizeHigh: var uint32): uint32 {.
importc: "GetFileSize", dynlib: "kernel32", stdcall.}
proc setFilePointerEx(hFile: uint, liDistanceToMove: int64,
lpNewFilePointer: ptr int64,
dwMoveMethod: uint32): int32 {.
importc: "SetFilePointerEx", dynlib: "kernel32", stdcall.}
const
NO_ERROR = IoErrorCode(0)
proc `==`*(a: IoErrorCode, b: uint32): bool {.inline.} =
(uint32(a) == b)
elif defined(posix):
import posix
const
DirSep* = '/'
AltSep* = '/'
BothSeps* = {'/'}
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)
var errno {.importc, header: "<errno.h>".}: cint
proc write(a1: cint, a2: pointer, a3: csize_t): int {.
importc, header: "<unistd.h>".}
proc read(a1: cint, a2: pointer, a3: csize_t): int {.
importc, header: "<unistd.h>".}
proc c_strerror(errnum: cint): cstring {.
importc: "strerror", header: "<string.h>".}
proc c_free(p: pointer) {.
importc: "free", header: "<stdlib.h>".}
proc getcwd(a1: cstring, a2: int): cstring {.
importc, header: "<unistd.h>", 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
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
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.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)
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)