diff --git a/doc/ru/scripting/builtins/libfile.md b/doc/ru/scripting/builtins/libfile.md index 6f35306d..e06e420c 100644 --- a/doc/ru/scripting/builtins/libfile.md +++ b/doc/ru/scripting/builtins/libfile.md @@ -183,3 +183,26 @@ file.join(директория: str, путь: str) --> str Соединяет путь. Пример: `file.join("world:data", "base/config.toml)` -> `world:data/base/config.toml`. Следует использовать данную функцию вместо конкатенации с `/`, так как `префикс:/путь` не является валидным. + +```lua +file.open(путь: str, режим: str) --> io_stream +``` + +Открывает поток для записи/чтения в файл по пути `путь`. + +Аргумент `режим` это список отдельных режимов, в котором каждый обозначается одним символом + +`r` - Чтение из файла +`w` - Запись в файл +`b` - Открыть поток в двоичном режиме (см. `../io_stream.md`) +`+` - Работает совместно с `w`. Добавляет к существующим данным новые (`append-mode`) + +```lua +file.open_named_pipe(имя: str, режим: str) -> io_stream +``` + +Открывает поток для записи/чтения в Named Pipe по пути `путь` + +`/tmp/` или `\\\\.\\pipe\\` добавлять не нужно - движок делает это автоматически. + +Доступные режимы такие же, как и в `file.open`, за исключением `+` \ No newline at end of file diff --git a/doc/ru/scripting/builtins/libtime.md b/doc/ru/scripting/builtins/libtime.md index d62a7af2..5bf9ad01 100644 --- a/doc/ru/scripting/builtins/libtime.md +++ b/doc/ru/scripting/builtins/libtime.md @@ -11,3 +11,21 @@ time.delta() -> float ``` Возвращает дельту времени (время прошедшее с предыдущего кадра) + +```python +time.utc_time() -> int +``` + +Возвращает время UTC в секундах + +```python +time.local_time() -> int +``` + +Возвращает локальное (системное) время в секундах + +```python +time.utc_offset() -> int +``` + +Возвращает смещение локального времени от UTC в секундах \ No newline at end of file diff --git a/doc/ru/scripting/extensions.md b/doc/ru/scripting/extensions.md index 32798e43..0de8e4a7 100644 --- a/doc/ru/scripting/extensions.md +++ b/doc/ru/scripting/extensions.md @@ -258,3 +258,9 @@ function sleep(timesec: number) ``` Вызывает остановку корутины до тех пор, пока не пройдёт количество секунд, указанное в **timesec**. Функция может быть использована только внутри корутины. + +```lua +os.pid -> number +``` + +Константа, в которой хранится PID текущего инстанса движка \ No newline at end of file diff --git a/doc/ru/scripting/io_stream.md b/doc/ru/scripting/io_stream.md new file mode 100644 index 00000000..16a978db --- /dev/null +++ b/doc/ru/scripting/io_stream.md @@ -0,0 +1,188 @@ +# Класс *io_stream* + +Класс, предназначенный для работы с потоками + +## Режимы + +Поток имеет три различных вида режима: + +**general** - Общий режим работы I/O +**binary** - Формат записи и чтения I/O +**flush** - Режим работы flush + +### general + +Имеет три режима: + +**default** - Дефолтный режим работы потока. При read может вернуть только часть от требуемых данных, при write сразу записывает данные в поток. + +**yield** - Почти тоже самое, что и **default**, но всегда будет возвращать все требуемые данные. Пока они не будут прочитаны, будет вызывать `coroutine.yield()`. Предназначен для работы в корутинах. + +**buffered** - Буферизирует записываемые и читаемые данные. + +При вызове `available`/`read` обновляет буфер чтения. + +После обновления в `read`, если буфер чтения переполнен, то бросает ошибку `buffer overflow`. + +Если требуемого кол-ва байт недостаточно в буфере для чтения, то бросает ошибку `buffer-underflow`. + +При вызове `write` записывает итоговые байты в буфер для записи. Если он переполнен, то бросает ошибку `buffer overflow`. + +При вызове `flush` проталкивает данные из буфера для записи в напрямую в поток + +### flush + +**all** - Сначала проталкивает данные из буфера напрямую в поток (если используется **buffered** режим), а после вызывает `flush` напрямую из библиотеки + +**buffer** - Только проталкивает данные из буфера в поток (если используется **buffered** режим) + +## Методы + +Методы, позволяющие изменить или получить различные режимы поведения потока + +```lua +-- Возвращает true, если поток используется в двоичном режиме +io_stream:is_binary_mode() --> bool + +-- Включает или выключает двоичный режим +io_stream:set_binary_mode(bool) + +-- Возвращает режим работы потока +io_stream:get_mode() --> string + +-- Задаёт режим работы потока. Выбрасывает ошибку, если передан неизвестный режим +io_stream:set_mode(string) + +-- Возвращает режим работы flush +io_stream:get_flush_mode() --> string + +-- Задаёт режим работы flush +io_stream:set_flush_mode(string) +``` + +I/O методы + +```lua + +--[[ +Читает данные из потока + +В двоичном режиме: + +Если arg - int, то читает из потока arg байт и возвращает ввиде Bytearray или таблицы, если useTable = true + +Если arg - string, то функция интерпретирует arg как шаблон для byteutil. Прочитает кол-во байт, которое определено шаблоном, передаст их в byteutil.unpack и вернёт результат + + +В текстовом режиме: + +Если arg - int, то читает нужное кол-во строк с окончанием CRLF/LF из arg и возвращает ввиде таблицы. Также, если trimEmptyLines = true, то удаляет пустые строки с начала и конца из итоговой таблицы + +Если arg не определён, то читает одну строку с окончанием CRLF/LF и возвращает её. +--]] +io_stream:read( + [опционально] arg: int | string, + [опционально] useTable | trimEmptyLines: bool +) --> Bytearray | table | string | table | ... + +--[[ +Записывает данные в поток + +В двоичном режиме: + +Если arg - string, то функция интерпретирует arg как шаблон для byteutil, передаст его и ... в byteutil.pack и результат запишет в поток + +Если arg - Bytearray | table, то записывает байты в поток + +В текстовом режиме: + +Если arg - string, то записывает строку в поток (вместе с окончанием LF) + +Если arg - table, то записывает каждую строку из таблицы отдельно +--]] +io_stream:write( + arg: Bytearray | table | string | table, + [опционально] ... +) + +-- Читает одну строку с окончанием CRLF/LF из потока вне зависимости от двоичного режима +io_stream:read_line() --> string + +-- Записывает одну строку с окончанием LF в поток вне зависимости от двоичного режима +io_stream:write_line(string) + +--[[ + +В двоичном режиме: + +Читает все доступные байты из потока и возвращает ввиде Bytearray или table, если useTable = true + +В текстовом режиме: + +Читает все доступные строки из потока в table если useTable = true, или в одну строку вместе с окончаниями, если нет + +--]] +io_stream:read_fully( + [опционально] useTable: bool +) --> Bytearray | table | table | string +``` + +Методы, имеющие смысл в использовании только в buffered режиме + +```lua +--[[ + +Если length определён, то возвращает true, если length байт доступно к чтению. Иначе возвращает false + +Если не определён, то возвращает количество байт, которое можно прочитать + +--]] +io_stream:available( + [опционально] length: int +) --> int | bool + +-- Возвращает максимальный размер буферов +io_stream:get_max_buffer_size() --> int + +-- Задаёт новый максимальный размер буферов +io_stream:set_max_buffer_size(max_size: int) +``` + +Методы, контролирующие состояние потока + +```lua + +-- Возвращает true, если поток открыт на данный момент +io_stream:is_alive() --> bool + +-- Возвращает true, если поток закрыт на данный момент +io_stream:is_closed() --> bool + +-- Закрывает поток +io_stream:close() + +--[[ + +Записывает все данные из write-буфера в поток в buffer/all flush-режимах +Вызывает ioLib.flush() в all flush-режиме + +--]] +io_stream:flush() +``` + +Создание нового потока + +```lua +--[[ + +Создаёт новый поток с переданным дескриптором и использующим переданную I/O библиотеку. (Более подробно в core:io_stream.lua) + +--]] +io_stream.new( + descriptor: int, + binaryMode: bool, + ioLib: table, + [опционально] mode: string = "default", + [опционально] flushMode: string = "all" +) -> io_stream +``` \ No newline at end of file diff --git a/res/modules/internal/stream_providers/file.lua b/res/modules/internal/stream_providers/file.lua new file mode 100644 index 00000000..80bc8c5b --- /dev/null +++ b/res/modules/internal/stream_providers/file.lua @@ -0,0 +1,17 @@ +local io_stream = require "core:io_stream" + +local lib = { + read = file.__read_descriptor, + write = file.__write_descriptor, + flush = file.__flush_descriptor, + is_alive = file.__has_descriptor, + close = file.__close_descriptor +} + +return function(path, mode) + return io_stream.new( + file.__open_descriptor(path, mode), + mode:find('b') ~= nil, + lib + ) +end \ No newline at end of file diff --git a/res/modules/internal/stream_providers/named_pipe.lua b/res/modules/internal/stream_providers/named_pipe.lua new file mode 100644 index 00000000..bdce841c --- /dev/null +++ b/res/modules/internal/stream_providers/named_pipe.lua @@ -0,0 +1,7 @@ +local FFI = ffi + +if FFI.os == "Windows" then + return require "core:internal/stream_providers/named_pipe_windows" +else + return require "core:internal/stream_providers/named_pipe_unix" +end \ No newline at end of file diff --git a/res/modules/internal/stream_providers/named_pipe_path_validate.lua b/res/modules/internal/stream_providers/named_pipe_path_validate.lua new file mode 100644 index 00000000..d2ad446d --- /dev/null +++ b/res/modules/internal/stream_providers/named_pipe_path_validate.lua @@ -0,0 +1,21 @@ +local forbiddenPaths = { + "/..\\", "\\../", + "/../", "\\..\\" +} + +return function(path) + local corrected = true + + if path:starts_with("../") or path:starts_with("..\\") then + corrected = false + else + for _, forbiddenPath in ipairs(forbiddenPaths) do + if path:find(forbiddenPath) then + corrected = false + break + end + end + end + + if not corrected then error "special path \"../\" is not allowed in path to named pipe" end +end \ No newline at end of file diff --git a/res/modules/internal/stream_providers/named_pipe_unix.lua b/res/modules/internal/stream_providers/named_pipe_unix.lua new file mode 100644 index 00000000..0a9c8904 --- /dev/null +++ b/res/modules/internal/stream_providers/named_pipe_unix.lua @@ -0,0 +1,104 @@ +local path_validate = require "core:internal/stream_providers/named_pipe_path_validate" +local io_stream = require "core:io_stream" + +local FFI = ffi + +FFI.cdef[[ +int open(const char *pathname, int flags); +int close(int fd); +ssize_t read(int fd, void *buf, size_t count); +ssize_t write(int fd, const void *buf, size_t count); +int fcntl(int fd, int cmd, ...); + +const char *strerror(int errnum); +]] + +local C = FFI.C + +local O_RDONLY = 0x0 +local O_WRONLY = 0x1 +local O_RDWR = 0x2 +local O_NONBLOCK = 0x800 +local F_GETFL = 3 + +local function getError() + local err = ffi.errno() + + return ffi.string(C.strerror(err)).." ("..err..")" +end + +local lib = {} + +function lib.read(fd, len) + local buffer = FFI.new("uint8_t[?]", len) + local result = C.read(fd, buffer, len) + + local out = Bytearray() + + if result <= 0 then + return out + end + + for i = 0, result - 1 do + out[i+1] = buffer[i] + end + + return out +end + +function lib.write(fd, bytearray) + local len = #bytearray + local buffer = FFI.new("uint8_t[?]", len) + for i = 1, len do + buffer[i-1] = bytearray[i] + end + + if C.write(fd, buffer, len) == -1 then + error("failed to write to named pipe: "..getError()) + end +end + +function lib.flush(fd) + -- no flush on unix +end + +function lib.is_alive(fd) + if fd == nil or fd < 0 then return false end + + return C.fcntl(fd, F_GETFL) ~= -1 +end + +function lib.close(fd) + C.close(fd) +end + +return function(path, mode) + path_validate(path) + + path = "/tmp/"..path + + local read = mode:find('r') ~= nil + local write = mode:find('w') ~= nil + + local flags + + if read and write then + flags = O_RDWR + elseif read then + flags = O_RDONLY + elseif write then + flags = O_WRONLY + else + error "mode must contain read or write flag" + end + + flags = bit.bor(flags, O_NONBLOCK) + + local fd = C.open(path, flags) + + if fd == -1 then + error("failed to open named pipe: "..getError()) + end + + return io_stream.new(fd, mode:find('b') ~= nil, lib) +end \ No newline at end of file diff --git a/res/modules/internal/stream_providers/named_pipe_windows.lua b/res/modules/internal/stream_providers/named_pipe_windows.lua new file mode 100644 index 00000000..83691aa5 --- /dev/null +++ b/res/modules/internal/stream_providers/named_pipe_windows.lua @@ -0,0 +1,144 @@ +local path_validate = require "core:internal/stream_providers/named_pipe_path_validate" +local io_stream = require "core:io_stream" + +local FFI = ffi + +FFI.cdef[[ +typedef void* HANDLE; +typedef uint32_t DWORD; +typedef int BOOL; +typedef void* LPVOID; +typedef const char* LPCSTR; + +BOOL CloseHandle(HANDLE hObject); +DWORD GetFileType(HANDLE hFile); +BOOL ReadFile(HANDLE hFile, void* lpBuffer, DWORD nNumberOfBytesToRead, + DWORD* lpNumberOfBytesRead, void* lpOverlapped); +BOOL WriteFile(HANDLE hFile, const void* lpBuffer, DWORD nNumberOfBytesToWrite, + DWORD* lpNumberOfBytesWritten, void* lpOverlapped); +HANDLE CreateFileA(LPCSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, + void* lpSecurityAttributes, DWORD dwCreationDisposition, + DWORD dwFlagsAndAttributes, HANDLE hTemplateFile); + +BOOL PeekNamedPipe( + HANDLE hNamedPipe, + LPVOID lpBuffer, + DWORD nBufferSize, + DWORD* lpBytesRead, + DWORD* lpTotalBytesAvail, + DWORD* lpBytesLeftThisMessage +); + +DWORD GetLastError(void); +BOOL FlushFileBuffers(HANDLE hFile); +]] + +local C = FFI.C + +local GENERIC_READ = 0x80000000 +local GENERIC_WRITE = 0x40000000 +local OPEN_EXISTING = 3 +local FILE_ATTRIBUTE_NORMAL = 0x00000080 +local FILE_TYPE_UNKNOWN = 0x0000 +local INVALID_HANDLE_VALUE = FFI.cast("HANDLE", -1) + +local lib = {} + +local function is_data_available(handle) + local bytes_available = FFI.new("DWORD[1]") + local success = FFI.C.PeekNamedPipe(handle, nil, 0, nil, bytes_available, nil) + + if success == 0 then + return -1 + end + + return bytes_available[0] > 0 +end + +function lib.read(handle, len) + local out = Bytearray() + + local has_data, err = is_data_available(handle) + + if not has_data then + return out + elseif hasData == -1 then + error("failed to read from named pipe: "..tostring(C.GetLastError())) + end + + local buffer = FFI.new("uint8_t[?]", len) + local read = FFI.new("DWORD[1]") + + local ok = C.ReadFile(handle, buffer, len, read, nil) + + if ok == 0 or read[0] == 0 then + return out + end + + for i = 0, read[0] - 1 do + out[i+1] = buffer[i] + end + + return out +end + +function lib.write(handle, bytearray) + local len = #bytearray + + local buffer = FFI.new("uint8_t[?]", len) + for i = 1, len do + buffer[i-1] = bytearray[i] + end + + local written = FFI.new("DWORD[1]") + + if C.WriteFile(handle, buffer, len, written, nil) == 0 then + error("failed to write to named pipe: "..tostring(C.GetLastError())) + end +end + +function lib.flush(handle) + C.FlushFileBuffers(handle) +end + +function lib.is_alive(handle) + if handle == nil or handle == INVALID_HANDLE_VALUE then + return false + else + return C.GetFileType(handle) ~= FILE_TYPE_UNKNOWN + end +end + +function lib.close(handle) + C.CloseHandle(handle) +end + +return function(path, mode) + path_validate(path) + + path = "\\\\.\\pipe\\"..path + + local read = mode:find('r') ~= nil + local write = mode:find('w') ~= nil + + local flags + + if read and write then + flags = bit.bor(GENERIC_READ, GENERIC_WRITE) + elseif read then + flags = GENERIC_READ + elseif write then + flags = GENERIC_WRITE + else + error("mode must contain read or write flag") + end + + local handle = C.CreateFileA(path, flags, 0, nil, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, nil) + + if handle == INVALID_HANDLE_VALUE then + error("failed to open named pipe: "..tostring(C.GetLastError())) + end + + return io_stream.new(handle, mode:find('b') ~= nil, lib) +end \ No newline at end of file diff --git a/res/modules/io_stream.lua b/res/modules/io_stream.lua new file mode 100644 index 00000000..185a29c2 --- /dev/null +++ b/res/modules/io_stream.lua @@ -0,0 +1,398 @@ +local io_stream = { } + +io_stream.__index = io_stream + +local MAX_BUFFER_SIZE = 8192 + +local DEFAULT_MODE = "default" +local BUFFERED_MODE = "buffered" +local YIELD_MODE = "yield" + +local ALL_MODES = { + DEFAULT_MODE, + BUFFERED_MODE, + YIELD_MODE +} + +local FLUSH_MODE_ALL = "all" +local FLUSH_MODE_ONLY_BUFFER = "buffer" + +local ALL_FLUSH_MODES = { + FLUSH_MODE_ALL, + FLUSH_MODE_ONLY_BUFFER +} + +local CR = string.byte('\r') +local LF = string.byte('\n') + +local function readFully(result, readFunc) + local isTable = type(result) == "table" + + local buf + + repeat + buf = readFunc(MAX_BUFFER_SIZE) + + if isTable then + for i = 1, #buf do + result[#result + 1] = buf[i] + end + else result:append(buf) end + until #buf == 0 +end + +--[[ + +descriptor - descriptor of stream for provided I/O library +binaryMode - if enabled, most methods will expect bytes instead of strings +ioLib - I/O library. Should include the following functions: + read(descriptor: int, length: int) -> Bytearray + May return bytearray with a smaller size if bytes have not arrived yet or have run out + write(descriptor: int, data: Bytearray) + flush(descriptor: int) + is_alive(descriptor: int) -> bool + close(descriptor: int) +--]] + +function io_stream.new(descriptor, binaryMode, ioLib, mode, flushMode) + mode = mode or DEFAULT_MODE + flushMode = flushMode or FLUSH_MODE_ALL + + local self = setmetatable({}, io_stream) + + self.descriptor = descriptor + self.binaryMode = binaryMode + self.maxBufferSize = MAX_BUFFER_SIZE + self.ioLib = ioLib + + self:set_mode(mode) + self:set_flush_mode(flushMode) + + return self +end + +function io_stream:is_binary_mode() + return self.binaryMode +end + +function io_stream:set_binary_mode(binaryMode) + self.binaryMode = binaryMode ~= nil +end + +function io_stream:get_mode() + return self.mode +end + +function io_stream:set_mode(mode) + if not table.has(ALL_MODES, mode) then + error("invalid stream mode: "..mode) + end + + if self.mode == BUFFERED_MODE then + self.writeBuffer:clear() + self.readBuffer:clear() + end + + if mode == BUFFERED_MODE and not self.writeBuffer then + self.writeBuffer = Bytearray() + self.readBuffer = Bytearray() + end + + self.mode = mode +end + +function io_stream:get_flush_mode() + return self.flushMode +end + +function io_stream:set_flush_mode(flushMode) + if not table.has(ALL_FLUSH_MODES, flushMode) then + error("invalid flush mode: "..flushMode) + end + + self.flushMode = flushMode +end + +function io_stream:get_max_buffer_size() + return self.maxBufferSize +end + +function io_stream:set_max_buffer_size(maxBufferSize) + self.maxBufferSize = maxBufferSize +end + +function io_stream:available(length) + if self.mode == BUFFERED_MODE then + self:__update_read_buffer() + + if not length then + return #self.readBuffer + else + return #self.readBuffer >= length + end + end +end + +function io_stream:__update_read_buffer() + local readed = Bytearray() + + readFully(readed, function(length) return self.ioLib.read(self.descriptor, length) end) + + self.readBuffer:append(readed) + + if #self.readBuffer > self.maxBufferSize then + error "buffer overflow" + end +end + +function io_stream:__read(length) + if self.mode == YIELD_MODE then + local buffer = Bytearray() + + while #buffer < length do + buffer:append(self.ioLib.read(self.descriptor, length - #buffer)) + + if #buffer < length then coroutine.yield() end + end + + return buffer + elseif self.mode == BUFFERED_MODE then + self:__update_read_buffer() + + if #self.readBuffer < length then + error "buffer underflow" + end + + local copy + + if #self.readBuffer == length then + copy = Bytearray() + + copy:append(self.readBuffer) + + self.readBuffer:clear() + else + copy = Bytearray() + + for i = 1, length do + copy[i] = self.readBuffer[i] + end + + self.readBuffer:remove(1, length) + end + + return copy + elseif self.mode == DEFAULT_MODE then + return self.ioLib.read(self.descriptor, length) + end +end + +function io_stream:__write(data) + if self.mode == BUFFERED_MODE then + self.writeBuffer:append(data) + + if #self.writeBuffer > self.maxBufferSize then + error "buffer overflow" + end + elseif self.mode == DEFAULT_MODE or self.mode == YIELD_MODE then + return self.ioLib.write(self.descriptor, data) + end +end + +function io_stream:read_fully(useTable) + if self.binaryMode then + local result = useTable and Bytearray() or { } + + readFully(result, function() return self:__read(self.maxBufferSize) end) + else + if useTable then + local lines = { } + + local line + + repeat + line = self:read_line() + + lines[#lines + 1] = line + until not line + + return lines + else + local result = Bytearray() + + readFully(result, function() return self:__read(self.maxBufferSize) end) + + return utf8.tostring(result) + end + end +end + +function io_stream:read_line() + local result = Bytearray() + + local first = true + + while true do + local char = self:__read(1) + + if #char == 0 then + if first then return else break end + end + + char = char[1] + + if char == LF then break + elseif char == CR then + char = self:__read(1) + + if char[1] == LF then break + else + result:append(CR) + result:append(char[1]) + end + else result:append(char) end + + first = false + end + + return utf8.tostring(result) +end + +function io_stream:write_line(str) + self:__write(utf8.tobytes(str .. LF)) +end + +function io_stream:read(arg, useTable) + local argType = type(arg) + + if self.binaryMode then + local byteArr + + if argType == "number" then + -- using 'arg' as length + + byteArr = self:__read(arg) + + if useTable == true then + local t = { } + + for i = 1, #byteArr do + t[i] = byteArr[i] + end + + return t + else + return byteArr + end + elseif argType == "string" then + return byteutil.unpack( + arg, + self:__read(byteutil.get_size(arg)) + ) + elseif argType == nil then + error( + "in binary mode the first argument must be a string data format".. + " for the library \"byteutil\" or the number of bytes to read" + ) + else + error("unknown argument type: "..argType) + end + else + if not arg then + return self:read_line() + else + local linesCount = arg + local trimLastEmptyLines = useTable or true + + if linesCount < 0 then error "count of lines to read must be positive" end + + local result = { } + + for i = 1, linesCount do + result[i] = self:read_line() + end + + if trimLastEmptyLines then + local i = #result + + while i >= 0 do + local length = utf8.length(result[i]) + + if length > 0 then break + else result[i] = nil end + + i = i - 1 + end + + local i = 1 + + while #result > 0 do + local length = utf8.length(result[i]) + + if length > 0 then break + else table.remove(result, i) end + end + end + + return result + end + end +end + +function io_stream:write(arg, ...) + local argType = type(arg) + + if self.binaryMode then + local byteArr + + if argType ~= "string" then + -- using arg as bytes table/bytearray + + if argType == "table" then + byteArr = Bytearray(arg) + else + byteArr = arg + end + else + byteArr = byteutil.pack(arg, ...) + end + + self:__write(byteArr) + else + if argType == "string" then + self:write_line(arg) + elseif argType == "table" then + for i = 1, #arg do + self:write_line(arg[i]) + end + else error("unknown argument type: "..argType) end + end +end + +function io_stream:is_alive() + return self.ioLib.is_alive(self.descriptor) +end + +function io_stream:is_closed() + return not self:is_alive() +end + +function io_stream:close() + if self.mode == BUFFERED_MODE then + self.readBuffer:clear() + self.writeBuffer:clear() + end + + return self.ioLib.close(self.descriptor) +end + +function io_stream:flush() + if self.mode == BUFFERED_MODE and #self.writeBuffer > 0 then + self.ioLib.write(self.descriptor, self.writeBuffer) + self.writeBuffer:clear() + end + + if self.flushMode ~= FLUSH_MODE_ONLY_BUFFER then self.ioLib.flush(self.descriptor) end +end + +return io_stream \ No newline at end of file diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index 765d2c08..a5142fdf 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -317,10 +317,30 @@ entities.get_all = function(uids) return stdcomp.get_all(uids) end end + local bytearray = require "core:internal/bytearray" + Bytearray = bytearray.FFIBytearray Bytearray_as_string = bytearray.FFIBytearray_as_string Bytearray_construct = function(...) return Bytearray(...) end + +file.open = require "core:internal/stream_providers/file" +file.open_named_pipe = require "core:internal/stream_providers/named_pipe" + +if ffi.os == "Windows" then + ffi.cdef[[ + unsigned long GetCurrentProcessId(); + ]] + + os.pid = ffi.C.GetCurrentProcessId() +else + ffi.cdef[[ + int getpid(void); + ]] + + os.pid = ffi.C.getpid() +end + ffi = nil math.randomseed(time.uptime() * 1536227939) @@ -473,6 +493,7 @@ function __vc_on_world_quit() _rules.clear() gui_util:__reset_local() stdcomp.__reset() + file.__close_all_descriptors() end local __vc_coroutines = {} @@ -629,4 +650,4 @@ function dofile(path) end end return _dofile(path) -end +end \ No newline at end of file diff --git a/src/logic/scripting/descriptors_manager.cpp b/src/logic/scripting/descriptors_manager.cpp new file mode 100644 index 00000000..f7458ccc --- /dev/null +++ b/src/logic/scripting/descriptors_manager.cpp @@ -0,0 +1,104 @@ +#include "logic/scripting/descriptors_manager.hpp" + +#include "debug/Logger.hpp" + +static debug::Logger logger("descriptors-manager"); + +namespace scripting { + + std::vector> descriptors_manager::descriptors; + + std::istream* descriptors_manager::get_input(int descriptor) { + if (!is_readable(descriptor)) + return nullptr; + + return descriptors[descriptor]->in.get(); + } + + std::ostream* descriptors_manager::get_output(int descriptor) { + if (!is_writeable(descriptor)) + return nullptr; + + return descriptors[descriptor]->out.get(); + } + + void descriptors_manager::flush(int descriptor) { + if (is_writeable(descriptor)) { + descriptors[descriptor]->out->flush(); + } + } + + bool descriptors_manager::has_descriptor(int descriptor) { + return is_readable(descriptor) || is_writeable(descriptor); + } + + bool descriptors_manager::is_readable(int descriptor) { + return descriptor >= 0 && descriptor < static_cast(descriptors.size()) + && descriptors[descriptor].has_value() + && descriptors[descriptor]->in != nullptr; + } + + bool descriptors_manager::is_writeable(int descriptor) { + return descriptor >= 0 && descriptor < static_cast(descriptors.size()) + && descriptors[descriptor].has_value() + && descriptors[descriptor]->out != nullptr; + } + + void descriptors_manager::close(int descriptor) { + if (descriptor >= 0 && descriptor < static_cast(descriptors.size())) { + if (descriptors[descriptor].has_value()) { + auto& desc = descriptors[descriptor].value(); + + if (desc.out) + desc.out->flush(); + + desc.in.reset(); + desc.out.reset(); + } + + descriptors[descriptor].reset(); + + descriptors[descriptor] = std::nullopt; + } + } + + int descriptors_manager::open_descriptor(const io::path& path, bool write, bool read) { + std::unique_ptr in; + std::unique_ptr out; + + try { + if (read) + in = io::read(path); + + if (write) + out = io::write(path); + } catch (const std::exception& e) { + logger.error() << "failed to open descriptor for " << path.string() + << ": " << e.what(); + + return -1; + } + + for (int i = 0; i < static_cast(descriptors.size()); ++i) { + if (!descriptors[i].has_value()) { + descriptors[i] = StreamDescriptor{ std::move(in), std::move(out) }; + return i; + } + } + + descriptors.emplace_back(StreamDescriptor{ std::move(in), std::move(out) }); + + return static_cast(descriptors.size() - 1); + } + + + void descriptors_manager::close_all_descriptors() { + for (int i = 0; i < static_cast(descriptors.size()); ++i) { + if (descriptors[i].has_value()) { + close(i); + } + } + + descriptors.clear(); + } +} \ No newline at end of file diff --git a/src/logic/scripting/descriptors_manager.hpp b/src/logic/scripting/descriptors_manager.hpp new file mode 100644 index 00000000..ccd1b67a --- /dev/null +++ b/src/logic/scripting/descriptors_manager.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "io/io.hpp" + +namespace scripting { + + struct StreamDescriptor { + std::unique_ptr in; + std::unique_ptr out; + }; + + class descriptors_manager { + private: + static std::vector> descriptors; + + public: + static std::istream* get_input(int descriptor); + static std::ostream* get_output(int descriptor); + + static void flush(int descriptor); + + static bool has_descriptor(int descriptor); + + static bool is_readable(int descriptor); + static bool is_writeable(int descriptor); + + static void close(int descriptor); + static int open_descriptor(const io::path& path, bool write, bool read); + + static void close_all_descriptors(); + }; +} \ No newline at end of file diff --git a/src/logic/scripting/lua/libs/libbyteutil.cpp b/src/logic/scripting/lua/libs/libbyteutil.cpp index 76bd9d37..54485fc1 100644 --- a/src/logic/scripting/lua/libs/libbyteutil.cpp +++ b/src/logic/scripting/lua/libs/libbyteutil.cpp @@ -197,9 +197,16 @@ static int l_tpack(lua::State* L) { return pack(L, format, true); } +static int l_get_size(lua::State* L) { + return lua::pushinteger( + L, static_cast(calc_size(lua::require_string(L, 1))) + ); +} + const luaL_Reg byteutillib[] = { {"pack", lua::wrap}, {"tpack", lua::wrap}, {"unpack", lua::wrap}, + {"get_size", lua::wrap}, {NULL, NULL} }; diff --git a/src/logic/scripting/lua/libs/libfile.cpp b/src/logic/scripting/lua/libs/libfile.cpp index 717ff52b..f5cb2bfc 100644 --- a/src/logic/scripting/lua/libs/libfile.cpp +++ b/src/logic/scripting/lua/libs/libfile.cpp @@ -9,6 +9,7 @@ #include "util/stringutil.hpp" #include "api_lua.hpp" #include "../lua_engine.hpp" +#include "logic/scripting/descriptors_manager.hpp" namespace fs = std::filesystem; using namespace scripting; @@ -258,6 +259,149 @@ static int l_create_zip(lua::State* L) { return 0; } +static int l_open_descriptor(lua::State* L) { + io::path path = lua::require_string(L, 1); + auto mode = lua::require_lstring(L, 2); + + bool write = mode.find('w') != std::string::npos; + bool read = mode.find('r') != std::string::npos; + + if (write && !is_writeable(path.entryPoint())) { + throw std::runtime_error("access denied"); + } + + if(!write && !read) { + throw std::runtime_error("mode must contain read or write flag"); + } + + if(write && read) { + throw std::runtime_error("random access file i/o is not supported"); + } + + bool wplusMode = write && mode.find('+') != std::string::npos; + + std::vector buffer; + + if(wplusMode) { + int temp_descriptor = scripting::descriptors_manager::open_descriptor(path, false, true); + + if (temp_descriptor == -1) { + throw std::runtime_error("failed to open descriptor for initial reading"); + } + + auto* in_stream = scripting::descriptors_manager::get_input(temp_descriptor); + + in_stream->seekg(0, std::ios::end); + std::streamsize size = in_stream->tellg(); + in_stream->seekg(0, std::ios::beg); + + buffer.resize(size); + in_stream->read(buffer.data(), size); + + scripting::descriptors_manager::close(temp_descriptor); + } + + int descriptor = scripting::descriptors_manager::open_descriptor(path, write, read); + + if(descriptor == -1) { + throw std::runtime_error("failed to open descriptor"); + } + + if(wplusMode) { + auto* out_stream = scripting::descriptors_manager::get_output(descriptor); + out_stream->write(buffer.data(), buffer.size()); + out_stream->flush(); + } + + return lua::pushinteger(L, descriptor); +} + +static int l_has_descriptor(lua::State* L) { + return lua::pushboolean(L, scripting::descriptors_manager::has_descriptor(lua::tointeger(L, 1))); +} + +static int l_read_descriptor(lua::State* L) { + int descriptor = lua::tointeger(L, 1); + + if (!scripting::descriptors_manager::has_descriptor(descriptor)) { + throw std::runtime_error("unknown descriptor"); + } + + if (!scripting::descriptors_manager::is_readable(descriptor)) { + throw std::runtime_error("descriptor is not readable"); + } + + int maxlen = lua::tointeger(L, 2); + + auto* stream = scripting::descriptors_manager::get_input(descriptor); + + util::Buffer buffer(maxlen); + + stream->read(buffer.data(), maxlen); + + std::streamsize read_len = stream->gcount(); + + return lua::create_bytearray(L, buffer.data(), read_len); +} + +static int l_write_descriptor(lua::State* L) { + int descriptor = lua::tointeger(L, 1); + + if (!scripting::descriptors_manager::has_descriptor(descriptor)) { + throw std::runtime_error("unknown descriptor"); + } + + if (!scripting::descriptors_manager::is_writeable(descriptor)) { + throw std::runtime_error("descriptor is not writeable"); + } + + auto data = lua::bytearray_as_string(L, 2); + + auto* stream = scripting::descriptors_manager::get_output(descriptor); + + stream->write(data.data(), static_cast(data.size())); + + if (!stream->good()) { + throw std::runtime_error("failed to write to stream"); + } + + return 0; +} + +static int l_flush_descriptor(lua::State* L) { + int descriptor = lua::tointeger(L, 1); + + if (!scripting::descriptors_manager::has_descriptor(descriptor)) { + throw std::runtime_error("unknown descriptor"); + } + + if (!scripting::descriptors_manager::is_writeable(descriptor)) { + throw std::runtime_error("descriptor is not writeable"); + } + + scripting::descriptors_manager::flush(descriptor); + + return 0; +} + +static int l_close_descriptor(lua::State* L) { + int descriptor = lua::tointeger(L, 1); + + if (!scripting::descriptors_manager::has_descriptor(descriptor)) { + throw std::runtime_error("unknown descriptor"); + } + + scripting::descriptors_manager::close(descriptor); + + return 0; +} + +static int l_close_all_descriptors(lua::State* L) { + scripting::descriptors_manager::close_all_descriptors(); + + return 0; +} + const luaL_Reg filelib[] = { {"exists", lua::wrap}, {"find", lua::wrap}, @@ -283,5 +427,12 @@ const luaL_Reg filelib[] = { {"mount", lua::wrap}, {"unmount", lua::wrap}, {"create_zip", lua::wrap}, + {"__open_descriptor", lua::wrap}, + {"__has_descriptor", lua::wrap}, + {"__read_descriptor", lua::wrap}, + {"__write_descriptor", lua::wrap}, + {"__flush_descriptor", lua::wrap}, + {"__close_descriptor", lua::wrap}, + {"__close_all_descriptors", lua::wrap}, {NULL, NULL} }; diff --git a/src/logic/scripting/lua/libs/libtime.cpp b/src/logic/scripting/lua/libs/libtime.cpp index 988e403a..dc878455 100644 --- a/src/logic/scripting/lua/libs/libtime.cpp +++ b/src/logic/scripting/lua/libs/libtime.cpp @@ -1,8 +1,13 @@ #include "engine/Engine.hpp" #include "api_lua.hpp" +#include using namespace scripting; +#if defined(_WIN32) || defined(_WIN64) + #define USE_MSVC_TIME_SAFE +#endif + static int l_uptime(lua::State* L) { return lua::pushnumber(L, engine->getTime().getTime()); } @@ -11,8 +16,57 @@ static int l_delta(lua::State* L) { return lua::pushnumber(L, engine->getTime().getDelta()); } +static int l_utc_time(lua::State* L) { + return lua::pushnumber(L, std::time(nullptr)); +} + +static int l_local_time(lua::State* L) { + std::time_t t = std::time(nullptr); + + std::tm gmt_tm{}; + std::tm local_tm{}; + +#if defined(USE_MSVC_TIME_SAFE) + gmtime_s(&gmt_tm, &t); + localtime_s(&local_tm, &t); +#else + gmtime_r(&t, &gmt_tm); + localtime_r(&t, &local_tm); +#endif + + std::time_t utc_time = std::mktime(&gmt_tm); + std::time_t local_time = std::mktime(&local_tm); + std::time_t offset = local_time - utc_time; + + return lua::pushnumber(L, t + offset); +} + +static int l_utc_offset(lua::State* L) { + std::time_t t = std::time(nullptr); + + std::tm gmt_tm{}; + std::tm local_tm{}; + +#if defined(USE_MSVC_TIME_SAFE) + gmtime_s(&gmt_tm, &t); + localtime_s(&local_tm, &t); +#else + gmtime_r(&t, &gmt_tm); + localtime_r(&t, &local_tm); +#endif + + std::time_t utc_time = std::mktime(&gmt_tm); + std::time_t local_time = std::mktime(&local_tm); + std::time_t offset = local_time - utc_time; + + return lua::pushnumber(L, offset); +} + const luaL_Reg timelib[] = { {"uptime", lua::wrap}, {"delta", lua::wrap}, + {"utc_time", lua::wrap}, + {"utc_offset", lua::wrap}, + {"local_time", lua::wrap}, {NULL, NULL} };