// SPDX-License-Identifier: LGPL-3.0-or-later /* OS extensions for Lua, module implementation. * * Copyright: 2022, The DoubleFourteen Code Forge * Author: Lorenzo Cogotti */ /** * Operating System eXtension module. * * @license LGPL-3.0-or-later * @copyright 2022, The DoubleFourteen Code Forge * @author Lorenzo Cogotti * @module osx */ #include "xconf.h" #include "osx.h" #include "osx_dir.h" #include "luacompat.h" #include "lua.h" #include "lualib.h" #include "lauxlib.h" #include #include #include #include #include #ifdef _WIN32 #include #include #include #include #include #include #include #include #include typedef struct __stat64 os_stat_type; #ifndef S_ISDIR #define S_ISDIR(mode) ((mode)&_S_IFDIR) #endif #ifndef S_ISREG #define S_ISREG(mode) ((mode)&_S_IFREG) #endif #define fileno(fh) _fileno(fh) #define stat(path, buf) _stat64(path, buf) #define fstat(fd, buf) _fstat64(fd, buf) #define mkdir(path, mode) _mkdir(path) #define getcwd(buf, len) _getcwd(buf, len) #define chdir(path) _chdir(path) #define getdrive() _getdrive() #define chdrive(drive) _chdrive(drive) #ifndef _O_WTEXT #define _O_WTEXT 0x10000 #endif #ifndef _O_U16TEXT #define _O_U16TEXT 0x20000 #endif #ifndef _O_U8TEXT #define _O_U8TEXT 0x40000 #endif #define setmode(fd, mode) _setmode(fd, mode) static int truncate(const char *path, __int64 size) { int fd; if (_sopen_s(&fd, path, _O_WRONLY|_O_BINARY|_O_NOINHERIT, _SH_DENYRW, 0) != 0) return -1; int ec = _chsize_s(fd, size); _close(fd); return ec; } #define ftruncate(fd, size) _chsize_s(fd, size) #define isatty(fd) _isatty(fd) #define fsync(fd) _commit(fd) static int flocking(FILE *fh, int mode, __int64 ofs, __int64 len) { if (ofs < 0 || len < 0 || len > LONG_MAX) { errno = ERANGE; return -1; } _lock_file(fh); if (_fseeki64(fh, ofs, SEEK_SET) != 0) goto fail; if (len == 0) { // NOTE: seek may flush write buffers, so file length must be taken now len = _filelengthi64(_fileno(fh)); if (len < 0) goto fail; len -= ofs; if (len < 0) len = 0; if (len > LONG_MAX) { errno = ERANGE; goto fail; } } if (_locking(_fileno(fh), mode, len) != 0) goto fail; _unlock_file(fh); return 0; fail: _unlock_file(fh); return -1; } #else #include #include #include #include typedef struct stat os_stat_type; #define _O_BINARY 0 #define _O_TEXT 0 #define _O_WTEXT 0 #define _O_U16TEXT 0 #define _O_U8TEXT 0 static inline int getdrive(void) { return 0; } static inline int chdrive(int drive) { (void) drive; errno = ENOTSUP; return -1; } static inline int setmode(int fd, int mode) { (void) fd, (void) mode; errno = ENOTSUP; return -1; } #define _LK_NBRLCK F_RDLCK #define _LK_NBLCK F_WRLCK #define _LK_UNLCK F_UNLCK static int flocking(FILE *fh, int mode, off_t ofs, off_t len) { if (ofs < 0 || len < 0) { errno = ERANGE; return -1; } flockfile(fh); if (fseeko(fh, ofs, SEEK_SET) != 0) goto fail; struct flock lk = { .l_whence = SEEK_SET, .l_type = mode, .l_start = ofs, .l_len = len // NOTE: 0 has special meaning }; if (fcntl(fileno(fh), F_SETLK, &lk) != 0) goto fail; funlockfile(fh); return 0; fail: funlockfile(fh); return -1; } #endif #ifdef __linux__ #define fadvise(fd, advice) posix_fadvise(fd, 0, 0, advice) #else // Stub posix_fadvise() #define POSIX_FADV_NORMAL 0 #define POSIX_FADV_SEQUENTIAL 0 #define POSIX_FADV_RANDOM 0 #define POSIX_FADV_NOREUSE 0 static inline int fadvise(int fd, int advice) { (void) fd, (void) advice; errno = ENOTSUP; return -1; } #endif static int os_pusherror(lua_State *L, const char *path) { int err = errno; lua_pushboolean(L, false); if (path) lua_pushfstring(L, "%s: %s", path, strerror(err)); else lua_pushstring(L, strerror(err)); lua_pushinteger(L, err); return 3; } static int os_pushfail(lua_State *L, const char *path) { int err = errno; luaL_pushfail(L); if (path) lua_pushfstring(L, "%s: %s", path, strerror(err)); else lua_pushstring(L, strerror(err)); lua_pushinteger(L, err); return 3; } static int os_result(lua_State *L, int ec, const char *path) { if (ec == 0) { lua_pushboolean(L, true); return 1; } return os_pusherror(L, path); } static df_os_dir *todir(lua_State *L) { df_os_dir *dir = luaL_checkudata(L, 1, DF_LUA_DIRHANDLE); if (dir->closeflag) luaL_error(L, "attempt to use a closed directory"); return dir; } static bool os_dir_filter(lua_State *L, const char *name) { // Preserve errno across filter calls int err = errno; lua_pushvalue(L, 2); lua_pushstring(L, name); lua_call(L, 1, 1); errno = err; bool result = lua_toboolean(L, -1); lua_pop(L, 1); return result; } static int os_dir_close(lua_State *L) { df_os_dir *dir = todir(L); os_closedir(dir->hn); dir->closeflag = true; return 0; } static int os_dir_tostring(lua_State *L) { df_os_dir *dir = luaL_checkudata(L, 1, DF_LUA_DIRHANDLE); if (dir->closeflag) { lua_pushliteral(L, "directory (closed)"); } else { df_os_dirhn hn = dir->hn; void *p; #ifdef _WIN32 p = (void *) hn->dirh; #else p = hn; #endif lua_pushfstring(L, "directory (%p)", p); } return 1; } static int os_dir_readdir(lua_State *L) { df_os_dir *dir = todir(L); errno = 0; const char *filename = os_readdir(dir->hn); if (!filename) return (errno == 0) ? 0 : os_pusherror(L, NULL); lua_pushstring(L, filename); return 1; } static int os_dir_dolistfiles(lua_State *L, bool closeflag, const char *path) { df_os_dir *dir = todir(L); bool dofilter = false; if (!lua_isnoneornil(L, 2)) { luaL_checktype(L, 2, LUA_TFUNCTION); dofilter = true; } char *filename; int i = 1; lua_newtable(L); errno = 0; while ((filename = os_readdir(dir->hn)) != NULL) { if (strcmp(filename, ".") == 0 || strcmp(filename, "..") == 0) continue; if (dofilter && !os_dir_filter(L, filename)) continue; lua_pushinteger(L, i++); lua_pushstring(L, filename); lua_settable(L, -3); } int err = errno; if (closeflag) os_dir_close(L); if (err != 0) { errno = err; return os_pushfail(L, path); } return 1; } static int os_dir_listfiles(lua_State *L) { return os_dir_dolistfiles(L, false, NULL); } static int os_dir_gc(lua_State *L) { df_os_dir *dir = luaL_checkudata(L, 1, DF_LUA_DIRHANDLE); if (!dir->closeflag) os_dir_close(L); return 0; } /** * Open a directory. * * @string path path to the directory to be opened * @treturn[1] Dir handle to the newly opened directory * @error[2] error message * @treturn[2] int system error code * @function opendir */ static int os_dir_open(lua_State *L) { const char *path = luaL_checkstring(L, 1); df_os_dir *dir = lua_newuserdata(L, sizeof(*dir)); dir->hn = os_opendir(path); dir->closeflag = false; if (!dir->hn) return os_pushfail(L, path); luaL_setmetatable(L, DF_LUA_DIRHANDLE); return 1; } static int os_dir_iter(lua_State *L); /** * Iterate directory. * * Enumerates directory contents one filename at a time, '.' and '..' are * filtered out. Function is intended to control a for loop. * * @string path path to directory to be traversed * @treturn string next file in directory * @raise error if the directory could not be opened, or an error occurs during its traversal. * @function dir */ static int os_dir(lua_State *L) { lua_pushcfunction(L, os_dir_open); lua_pushvalue(L, 1); lua_call(L, 1, 2); if (lua_isnil(L, -2)) lua_error(L); lua_pop(L, 1); lua_pushcclosure(L, os_dir_iter, 2); return 1; } static int os_dir_iter(lua_State *L) { const char *path = lua_tostring(L, lua_upvalueindex(1)); df_os_dir *dir = lua_touserdata(L, lua_upvalueindex(2)); const char *filename; errno = 0; nextfilename: filename = os_readdir(dir->hn); if (!filename) { // Error or end of directory if (errno != 0) luaL_error(L, lua_pushfstring(L, "%s: %s", path, strerror(errno))); return 0; } // Skip dot and dotdot if (strcmp(filename, ".") == 0 || strcmp(filename, "..") == 0) goto nextfilename; // New directory entry lua_pushstring(L, filename); return 1; } /** * List directory contents. * * @string path path to directory to be listed * @tparam[opt] function filter a function taking a filename string and returning * a boolean to accept/reject each entry * @treturn[1] {string,...} array of filenames within the directory, * excluding '.' and '..' * @error[2] error message * @treturn[2] int system error code * @function listfiles */ static int os_listfiles(lua_State *L) { const char *path = luaL_checkstring(L, 1); if (lua_isnone(L, 2)) lua_pushnil(L); lua_pushcfunction(L, os_dir_open); lua_pushvalue(L, 1); lua_call(L, 1, 2); if (lua_isnil(L, -2)) lua_error(L); lua_copy(L, -2, 1); lua_pop(L, 2); return os_dir_dolistfiles(L, true, path); } /** * File info returned on success by @{stat}. * * @string type file type, either of 'regular', 'directory', or 'other', for * regular files, directories and any other file respectively * @int mtime last modified time in seconds since Unix Epoch, only available for * 'regular' and 'directory' * @int size file size in bytes, only available for 'regular' files * @table FileStat */ /** * Get file info. * * @tparam ?string|File f path or opened file handle * @treturn[1] FileStat on success * @error[2] error message * @treturn[2] int system error code * @function stat */ static int os_stat(lua_State *L) { const char *path = NULL; os_stat_type buf; int ec; if (lua_isstring(L, 1)) { path = lua_tostring(L, 1); ec = stat(path, &buf); } else { luaL_Stream *stream = luaL_checkudata(L, 1, LUA_FILEHANDLE); ec = fstat(fileno(stream->f), &buf); } if (ec != 0) return os_pushfail(L, path); lua_createtable(L, 0, 3); if (S_ISREG(buf.st_mode)) { lua_pushliteral(L, "regular"); lua_setfield(L, -2, "type"); lua_pushinteger(L, buf.st_size); lua_setfield(L, -2, "size"); lua_pushinteger(L, (lua_Integer) buf.st_mtime); lua_setfield(L, -2, "mtime"); } else if (S_ISDIR(buf.st_mode)) { lua_pushliteral(L, "directory"); lua_setfield(L, -2, "type"); lua_pushinteger(L, (lua_Integer) buf.st_mtime); lua_setfield(L, -2, "mtime"); } else { lua_pushstring(L, "other"); lua_setfield(L, -2, "type"); } return 1; } /** * Create a new directory. * * @string path path to the directory to be created * @treturn[1] bool true on success and a new directory is created at path * @treturn[2] bool false on failure (note: directory may already exist) * @treturn[2] string error message * @treturn[2] int system error code * @function mkdir */ static int os_mkdir(lua_State *L) { const char *path = luaL_checkstring(L, 1); int ec = mkdir(path, 0666); return os_result(L, ec, path); } /** * Get process current working directory. * * @treturn[1] string path to current working directory on success * @error[2] error message * @treturn[2] int system error code * @function getcwd */ static int os_getcwd(lua_State *L) { #if LUA_VERSION_NUM >= 502 luaL_Buffer buf; char *p; size_t size = 128; luaL_buffinit(L, &buf); while (true) { p = luaL_prepbuffsize(&buf, size); if (getcwd(p, size)) break; // got working directory if (errno != ERANGE) return os_pusherror(L, NULL); size <<= 1; if (size == 0) { // PARANOID errno = ERANGE; return os_pusherror(L, NULL); } } luaL_pushresultsize(&buf, strlen(p)); return 1; #else // Unfortunately this leaks memory if Lua ever decides to longjmp(). // After 12 years since Lua 5.2 introduction, // that's what we get with LuaJIT. char *buf = NULL; size_t size = 128; while (true) { char *p = realloc(buf, size); if (!p) goto fail; buf = p; if (getcwd(buf, size)) break; // got working directory if (errno != ERANGE) goto fail; size <<= 1; if (size == 0) { // PARANOID errno = ERANGE; goto fail; } } lua_pushstring(L, buf); free(buf); return 1; fail: free(buf); return os_pusherror(L, NULL); #endif } /** * Change process current working directory. * * @string path path to the new working directory * @treturn[1] boolean true on success * @treturn[2] boolean false on failure * @treturn[2] string error message * @treturn[2] int system error code * @function chdir */ static int os_chdir(lua_State *L) { const char *path = luaL_checkstring(L, 1); int ec = chdir(path); return os_result(L, ec, path); } /** * Get the active drive letter. * * @treturn[1] string the current drive letter on systems supporting multiple drives * (e.g. Windows), an empty string otherwise * @error[2] error message * @treturn[2] int system error code * @function getdrive */ static int os_getdrive(lua_State *L) { errno = 0; int drive = getdrive(); if (drive == 0) { if (errno != 0) return os_pusherror(L, NULL); lua_pushliteral(L, ""); return 1; } char letter = 'A' + drive - 1; lua_pushlstring(L, &letter, 1); return 1; } /** * Change active drive. * * May only succeed on systems with multiple drives (e.g. Windows). * On any other system this function always fail with an appropriate error. * * @string drive new drive letter (e.g. 'A', 'C', 'D') * @treturn[1] bool true on success * @treturn[2] bool false on failure * @treturn[2] string error message * @treturn[2] int system error code * @function chdrive */ static int os_chdrive(lua_State *L) { size_t len; const char *s = luaL_checklstring(L, 1, &len); if (len != 1) luaL_argerror(L, 1, "invalid drive"); int drive = *s - 'A' + 1; if (drive < 1 || drive > 26) luaL_argerror(L, 1, "invalid drive"); int ec = chdrive(drive); return os_result(L, ec, NULL); } /** * Set file mode. * * May only succeed on systems where multiple file mode exist (e.g. Windows), * will always fail anywhere else (e.g. Unix). * Typically such error can be ignored. * * @tparam File f a file opened for read * @string mode new mode for file, either of 'binary', 'text', 'u8text', * 'u16text' or 'wtext' * @treturn[1] bool true on success * @treturn[2] bool false on failure * @treturn[2] string error message * @treturn[2] int system error code * @function setmode */ static int os_setmode(lua_State *L) { static const char *modes[] = { "binary", "text", "u8text", "u16text", "wtext", NULL }; static const int imodes[] = { _O_BINARY, _O_TEXT, _O_U8TEXT, _O_U16TEXT, _O_WTEXT }; luaL_Stream *stream = luaL_checkudata(L, 1, LUA_FILEHANDLE); int i = luaL_checkoption(L, 2, NULL, modes); int ec = setmode(fileno(stream->f), imodes[i]); return os_result(L, ec, NULL); } /** * Change file size. * * @tparam ?string|File f path or file opened for write * @int size new file size in bytes * @treturn[1] bool true on success * @treturn[2] bool false on failure * @treturn[2] string error string * @treturn[2] int system error code * @function chsize */ static int os_chsize(lua_State *L) { lua_Integer size = luaL_checkinteger(L, 2); if (size < 0) size = 0; const char *path = NULL; int ec; if (lua_isstring(L, 1)) { path = lua_tostring(L, 1); ec = truncate(path, size); } else { luaL_Stream *stream = luaL_checkudata(L, 1, LUA_FILEHANDLE); ec = ftruncate(fileno(stream->f), size); } return os_result(L, ec, path); } /** * Advise kernel on file access pattern. * * @tparam File f a file opened for read * @string advise read access pattern (either of: 'normal', 'sequential', 'random', 'noreuse') * @treturn[1] boolean true on success * @treturn[2] boolean false on failure * @treturn[2] string error message * @treturn[2] int system error code * @function fadvise */ static int os_fadvise(lua_State *L) { static const char *hints[] = { "normal", "sequential", "random", "noreuse", NULL }; static const int ihints[] = { POSIX_FADV_NORMAL, POSIX_FADV_SEQUENTIAL, POSIX_FADV_RANDOM, POSIX_FADV_NOREUSE }; luaL_Stream *stream = luaL_checkudata(L, 1, LUA_FILEHANDLE); int i = luaL_checkoption(L, 2, NULL, hints); int ec = fadvise(fileno(stream->f), ihints[i]); return os_result(L, ec, NULL); } /** * Test whether an opened file handle references a character device (TTY). * * @tparam File f an opened file handle * @treturn boolean true if file references a character device, false otherwise. * @function isatty */ static int os_isatty(lua_State *L) { luaL_Stream *stream = luaL_checkudata(L, 1, LUA_FILEHANDLE); int ec = isatty(fileno(stream->f)); lua_pushboolean(L, ec != 0); return 1; } /** * Commit any pending write to disk. * * @tparam File f a file opened for write * @treturn[1] boolean true on success * @treturn[2] boolean false on failure * @treturn[2] string error message * @treturn[2] int system error code * @function commit */ static int os_commit(lua_State *L) { luaL_Stream *stream = luaL_checkudata(L, 1, LUA_FILEHANDLE); int ec = fsync(fileno(stream->f)); return os_result(L, ec, NULL); } /** * Place or remove a read (shared) or write (exclusive) lock to a file. * * @tparam File f a file opened with appropriate mode for the requested lock * @string mode lock mode, any of 'r' (read), 'w' (write), 'u' (release lock) * @int[opt=0] start initial offset for the requested section * @int[optchain=0] len length of the requested section, 0 has a special meaning, * indicating the whole file starting from the initial offset * @treturn[1] boolean true on success * @treturn[2] boolean false on failure * @treturn[2] string error message * @treturn[2] int system error code * @function locking */ static int os_locking(lua_State *L) { static const char *modes[] = { "r", "w", "u", NULL }; static const int imodes[] = { _LK_NBRLCK, _LK_NBLCK, _LK_UNLCK }; luaL_Stream *stream = luaL_checkudata(L, 1, LUA_FILEHANDLE); int i = luaL_checkoption(L, 2, NULL, modes); lua_Integer ofs = luaL_optinteger(L, 3, 0); lua_Integer len = luaL_optinteger(L, 4, 0); int ec = flocking(stream->f, imodes[i], ofs, len); return os_result(L, ec, NULL); } /** * preferred path separator char: '\\' on Windows, '/' anywhere else * * @string sep */ /** * Directory handle. * * Allows convenient file traversal on directories. * Entries traversal order is system-specific, and may not be ordered at all. * Provides metamethods: __gc to close directories on garbage collect, * __close (on Lua 5.4) to close directories automatically, and __tostring for * adequate formatting. * * @type Dir */ /** * Read next directory entry. * * @treturn[1] string next filename in directory, '.' and '..' are also returned * @error[2] error message on failure * @treturn[2] int system error code * @raise error message when attempting to read from a closed directory * @function readdir */ /** * Read and filter subsequent directory entries. * * @tparam[opt] function filter a function taking a filename string and returning * a boolean to accept/reject each entry * @treturn[1] {string,...} subsequent filtered contents, '.' and '..' are always filtered out. * @error[2] error message on failure * @treturn[2] int system error code * @raise error message when attempting to read from a closed directory * @function listfiles */ /** * Close opened directory. * * @raise error message when closing a directory twice * @function close */ static void createmeta(lua_State *L) { static const luaL_Reg metameth[] = { { "__index", NULL }, // placeholder { "__tostring", os_dir_tostring }, { "__gc", os_dir_gc }, #if LUA_VERSION_NUM >= 504 { "__close", os_dir_gc}, #endif { NULL, NULL } }; static const luaL_Reg meth[] = { { "readdir", os_dir_readdir }, { "listfiles", os_dir_listfiles }, { "close", os_dir_close }, { NULL, NULL } }; luaL_newmetatable(L, DF_LUA_DIRHANDLE); luaL_setfuncs(L, metameth, 0); luaL_newlibtable(L, meth); luaL_setfuncs(L, meth, 0); lua_setfield(L, -2, "__index"); lua_pop(L, 1); } DF_OSXMOD_API int luaopen_osx(lua_State *L) { static const luaL_Reg funcs[] = { { "opendir", os_dir_open }, { "dir", os_dir }, { "listfiles", os_listfiles }, { "stat", os_stat }, { "mkdir", os_mkdir }, { "getcwd", os_getcwd }, { "chdir", os_chdir }, { "getdrive", os_getdrive }, { "chdrive", os_chdrive }, { "setmode", os_setmode }, { "chsize", os_chsize }, { "fadvise", os_fadvise }, { "isatty", os_isatty }, { "commit", os_commit }, { "locking", os_locking }, { NULL, NULL } }; createmeta(L); luaL_newlib(L, funcs); lua_createtable(L, 0, 1); luaL_requiref(L, LUA_OSLIBNAME, luaopen_os, false); lua_setfield(L, -2, "__index"); lua_setmetatable(L, -2); #ifdef _WIN32 lua_pushliteral(L, "\\"); #else lua_pushliteral(L, "/"); #endif lua_setfield(L, -2, "sep"); return 1; }