lua-osx/osx.c

978 lines
22 KiB
C

// 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 <assert.h>
#include <errno.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/locking.h>
#include <direct.h>
#include <fcntl.h>
#include <windows.h>
#include <io.h>
#include <share.h>
#include <limits.h>
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 <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
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;
}