diff --git a/CHANGELOG.md b/CHANGELOG.md index d1c430d..6b1bb73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,15 +14,13 @@ The scope of what is covered by the version number excludes: - create a release branch - update the changelog below -- update version and copyright-years in `./LICENSE.md` and `./src/[module-name]/init.lua` (in doc-comments - header, and in module constants) +- update version and copyright-years in `./LICENSE.md` and `./src/time.c` (in module constants) - create a new rockspec and update the version inside the new rockspec:
- `cp [module-name]-scm-1.rockspec ./rockspecs/[module-name]-X.Y.Z-1.rockspec` -- test: run `make test` and `make lint` -- clean and render the docs: run `make clean` and `make docs` -- commit the changes as `release X.Y.Z` + `cp luasystem-scm-0.rockspec ./rockspecs/luasystem-X.Y.Z-1.rockspec` +- clean and render the docs: run `ldoc .` +- commit the changes as `Release vX.Y.Z` - push the commit, and create a release PR -- after merging tag the release commit with `X.Y.Z` +- after merging tag the release commit with `vX.Y.Z` - upload to LuaRocks:
`luarocks upload ./rockspecs/[module-name]-X.Y.Z-1.rockspec --api-key=ABCDEFGH` - test the newly created rock:
@@ -32,9 +30,20 @@ The scope of what is covered by the version number excludes: ### Version X.Y.Z, unreleased -- a fix -- a change +- Change: `gettime` is deprecated. Use `unixtime` instead. +- Feat: `unixtime` returns the time since the Unix epoch. +- Feat: `windowstime` returns the time since the Windows epoch. +- Feat: `time` returns the system time. +- Feat: `setenv` added to set environment variables. +- Feat: `getenvs` added to list environment variables. +- Feat: `random` added to return high-quality random bytes -### Version 0.1.0, released 01-Jan-2022 +### Version 0.2.1, released 02-Oct-2016 + +### Version 0.2.0, released 08-May-2016 + +### Version 0.1.1, released 10-Apr-2016 + +### Version 0.1.0, released 11-Feb-2016 - initial release diff --git a/luasystem-scm-0.rockspec b/luasystem-scm-0.rockspec index 74e301d..be206c5 100644 --- a/luasystem-scm-0.rockspec +++ b/luasystem-scm-0.rockspec @@ -11,7 +11,7 @@ version = package_version.."-"..rockspec_revision source = { url = "git+https://github.com/"..github_account_name.."/"..github_repo_name..".git", branch = (package_version == "scm") and "master" or nil, - tag = (package_version ~= "scm") and package_version or nil, + tag = (package_version ~= "scm") and "v"..package_version or nil, } description = { @@ -45,7 +45,7 @@ local function make_platform(plat) return { modules = { ['system.core'] = { - sources = { 'src/core.c', 'src/compat.c', 'src/time.c', }, + sources = { 'src/core.c', 'src/compat.c', 'src/time.c', 'src/environment.c', 'src/random.c' }, defines = defines[plat], libraries = libraries[plat], }, diff --git a/rockspecs/luasystem-0.2.1-1.rockspec b/rockspecs/luasystem-0.2.1-1.rockspec index 7d8b9b0..e14118d 100644 --- a/rockspecs/luasystem-0.2.1-1.rockspec +++ b/rockspecs/luasystem-0.2.1-1.rockspec @@ -11,7 +11,7 @@ version = package_version.."-"..rockspec_revision source = { url = "git+https://github.com/"..github_account_name.."/"..github_repo_name..".git", branch = (package_version == "scm") and "master" or nil, - tag = (package_version ~= "scm") and package_version or nil, + tag = (package_version ~= "scm") and "v"..package_version or nil, } description = { diff --git a/spec/01-time_spec.lua b/spec/01-time_spec.lua new file mode 100644 index 0000000..49fdd18 --- /dev/null +++ b/spec/01-time_spec.lua @@ -0,0 +1,77 @@ +local system = require 'system.core' + +describe('Test time functions', function() + + -- returns the new second, on the new second + local function wait_for_second_rollover() + local start_time = math.floor(os.time()) + local end_time = math.floor(os.time()) + while end_time == start_time do + end_time = math.floor(os.time()) + end + return end_time + end + + + describe("time()", function() + + it('returns current time', function() + wait_for_second_rollover() + local lua_time = wait_for_second_rollover() + local aaa_time = system.time() + assert.is.near(lua_time, aaa_time, 0.01) + + wait_for_second_rollover() + assert.is.near(1, system.time() - aaa_time, 0.01) + end) + + end) + + + + describe("monotime()", function() + + it('returns monotonically increasing time', function() + local starttime = system.monotime() + local endtime = system.monotime() + local delta = endtime - starttime + assert.is_true(starttime > 0) + assert.is_true(delta >= 0) + assert.is_true(system.monotime() - endtime >= 0) + end) + + end) + + + + describe("sleep()", function() + + it("should sleep for the specified time", function() + local start_time = wait_for_second_rollover() + system.sleep(1) + local end_time = os.time() + local elapsed_time = end_time - start_time + assert.is.near(elapsed_time, 1, 0.01) + end) + + + it("should sleep for the specified time; fractional", function() + local start_time = system.time() + system.sleep(0.5) + local end_time = system.time() + local elapsed_time = end_time - start_time + assert.is.near(0.5, elapsed_time, 0.01) + end) + + + it("should return immediately for a non-positive sleep time", function() + local start_time = wait_for_second_rollover() + system.sleep(-1) + local end_time = os.time() + local elapsed_time = end_time - start_time + assert.is.near(elapsed_time, 0, 0.001) + end) + + end) + +end) diff --git a/spec/02-random_spec.lua b/spec/02-random_spec.lua new file mode 100644 index 0000000..ca6645d --- /dev/null +++ b/spec/02-random_spec.lua @@ -0,0 +1,47 @@ +local system = require("system") + +describe("Random:", function() + + describe("random()", function() + + it("should return random bytes for a valid number of bytes", function() + local num_bytes = system.MAX_RANDOM_BUFFER_SIZE + local result, err_msg = system.random(num_bytes) + assert.is_nil(err_msg) + assert.is.string(result) + assert.is_equal(num_bytes, #result) + end) + + + it("should return an error message for an invalid number of bytes", function() + local num_bytes = 0 + local result, err_msg = system.random(num_bytes) + assert.is.falsy(result) + assert.are.equal("invalid number of bytes, must be between 1 and 1024", err_msg) + end) + + + it("should return an error message for exceeding the maximum buffer size", function() + local num_bytes = system.MAX_RANDOM_BUFFER_SIZE + 1 + local result, err_msg = system.random(num_bytes) + assert.is.falsy(result) + assert.are.equal("invalid number of bytes, must be between 1 and 1024", err_msg) + end) + + + it("should not return duplicate results", function() + local num_bytes = 10 + local result1, err_msg = system.random(num_bytes) + assert.is_nil(err_msg) + assert.is.string(result1) + + local result2, err_msg = system.random(num_bytes) + assert.is_nil(err_msg) + assert.is.string(result2) + + assert.is_not.equal(result1, result2) + end) + + end) + +end) diff --git a/spec/03-environment_spec.lua b/spec/03-environment_spec.lua new file mode 100644 index 0000000..dc7fe2a --- /dev/null +++ b/spec/03-environment_spec.lua @@ -0,0 +1,76 @@ +-- Import the library that contains the environment-related functions +local system = require("system") + +describe("Environment Variables:", function() + + describe("setenv()", function() + + it("should set an environment variable", function() + assert.is_true(system.setenv("TEST_VAR", "test_value")) + assert.is_equal("test_value", os.getenv("TEST_VAR")) + end) + + + it("should set an empty environment variable value", function() + assert.is_true(system.setenv("TEST_VAR", "")) + assert.is_equal("", os.getenv("TEST_VAR")) + end) + + + it("should set an empty environment variable", function() + assert.is_true(system.setenv("TEST_VAR", "")) + assert.is_equal("", os.getenv("TEST_VAR")) + end) + + + it("should error on input bad type", function() + assert.has_error(function() + system.setenv("TEST_VAR", {}) + end) + assert.has_error(function() + system.setenv({}, "test_value") + end) + end) + + + it("should return success on deleting a variable that doesn't exist", function() + if os.getenv("TEST_VAR") ~= nil then + -- clear if it was already set + assert.is_true(system.setenv("TEST_VAR", nil)) + end + + assert.is_true(system.setenv("TEST_VAR", nil)) -- clear again shouldn't fail + end) + + end) + + + + describe("getenvs()", function() + + it("should list environment variables", function() + assert.is_true(system.setenv("TEST_VAR1", nil)) + assert.is_true(system.setenv("TEST_VAR2", nil)) + assert.is_true(system.setenv("TEST_VAR3", nil)) + local envVars1 = system.getenvs() + assert.is_true(system.setenv("TEST_VAR1", "test_value1")) + assert.is_true(system.setenv("TEST_VAR2", "test_value2")) + assert.is_true(system.setenv("TEST_VAR3", "test_value3")) + local envVars2 = system.getenvs() + assert.is_true(system.setenv("TEST_VAR1", nil)) + assert.is_true(system.setenv("TEST_VAR2", nil)) + assert.is_true(system.setenv("TEST_VAR3", nil)) + + for k,v in pairs(envVars1) do + envVars2[k] = nil + end + assert.are.same({ + TEST_VAR1 = "test_value1", + TEST_VAR2 = "test_value2", + TEST_VAR3 = "test_value3", + }, envVars2) + end) + + end) + +end) diff --git a/spec/time_spec.lua b/spec/time_spec.lua deleted file mode 100644 index a017cfe..0000000 --- a/spec/time_spec.lua +++ /dev/null @@ -1,31 +0,0 @@ -local system = require 'system.core' - -describe('Test time functions', function() - it('gettime returns current time', function() - local starttime = system.gettime() - local expected = os.time() - local endtime = system.gettime() - local delta = endtime - starttime - local avg = starttime + delta/2 - assert.is_true(expected >= math.floor(starttime)) - assert.is_true(expected <= math.ceil(endtime)) - assert.is_near(expected, avg, 1 + delta) - end) - - it('monottime returns monotonically increasing time', function() - local starttime = system.monotime() - local endtime = system.monotime() - local delta = endtime - starttime - assert.is_true(starttime > 0) - assert.is_true(delta >= 0) - assert.is_true(system.monotime() - endtime >= 0) - end) - - it('sleep will wait for specified amount of time', function() - local starttime = system.gettime() - local starttick = system.monotime() - system.sleep(0.5) - assert.is_near(0.5, system.gettime() - starttime, 0.15) - assert.is_near(0.5, system.monotime() - starttick, 0.15) - end) -end) diff --git a/src/core.c b/src/core.c index 6778402..c3a3305 100644 --- a/src/core.c +++ b/src/core.c @@ -13,6 +13,8 @@ #endif void time_open(lua_State *L); +void environment_open(lua_State *L); +void random_open(lua_State *L); /*------------------------------------------------------------------------- * Initializes all library modules. @@ -23,5 +25,7 @@ LUAEXPORT int luaopen_system_core(lua_State *L) { lua_pushstring(L, LUASYSTEM_VERSION); lua_rawset(L, -3); time_open(L); + random_open(L); + environment_open(L); return 1; } diff --git a/src/environment.c b/src/environment.c new file mode 100644 index 0000000..8a39c34 --- /dev/null +++ b/src/environment.c @@ -0,0 +1,124 @@ +/// @submodule system +#include +#include +#include "compat.h" +#include +#include + + +/*** +Returns a table with all environment variables. +@function getenvs +@treturn table table with all environment variables and their values +*/ +static int lua_list_environment_variables(lua_State* L) { + lua_newtable(L); + +#ifdef _WIN32 + char* envStrings = GetEnvironmentStrings(); + char* envString = envStrings; + + if (envStrings == NULL) { + lua_pushnil(L); + return 1; + } + + while (*envString != '\0') { + const char* envVar = envString; + + // Split the environment variable into key and value + char* equals = strchr(envVar, '='); + if (equals != NULL) { + lua_pushlstring(L, envVar, equals - envVar); // Push the key + lua_pushstring(L, equals + 1); // Push the value + lua_settable(L, -3); // Set the key-value pair in the table + } + + envString += strlen(envString) + 1; + } + + FreeEnvironmentStrings(envStrings); +#else + extern char** environ; + + if (environ != NULL) { + for (char** envVar = environ; *envVar != NULL; envVar++) { + const char* envVarStr = *envVar; + + // Split the environment variable into key and value + char* equals = strchr(envVarStr, '='); + if (equals != NULL) { + lua_pushlstring(L, envVarStr, equals - envVarStr); // Push the key + lua_pushstring(L, equals + 1); // Push the value + lua_settable(L, -3); // Set the key-value pair in the table + } + } + } +#endif + + return 1; +} + + +/*** +Sets an environment variable. +@function setenv +@tparam string name name of the environment variable +@tparam[opt] string value value of the environment variable, if nil the variable will be deleted +@treturn boolean success +*/ +static int lua_set_environment_variable(lua_State* L) { + const char* variableName = luaL_checkstring(L, 1); + const char* variableValue = luaL_optstring(L, 2, NULL); + +#ifdef _WIN32 + if (variableValue == NULL) { + // If the value is nil, delete the environment variable + if (SetEnvironmentVariable(variableName, NULL)) { + lua_pushboolean(L, 1); + } else { + lua_pushboolean(L, 0); + } + } else { + // Set the environment variable with the provided value + if (SetEnvironmentVariable(variableName, variableValue)) { + lua_pushboolean(L, 1); + } else { + lua_pushboolean(L, 0); + } + } +#else + if (variableValue == NULL) { + // If the value is nil, delete the environment variable + if (unsetenv(variableName) == 0) { + lua_pushboolean(L, 1); + } else { + lua_pushboolean(L, 0); + } + } else { + // Set the environment variable with the provided value + if (setenv(variableName, variableValue, 1) == 0) { + lua_pushboolean(L, 1); + } else { + lua_pushboolean(L, 0); + } + } +#endif + + return 1; +} + + + +static luaL_Reg func[] = { + { "setenv", lua_set_environment_variable }, + { "getenvs", lua_list_environment_variables }, + { NULL, NULL } +}; + +/*------------------------------------------------------------------------- + * Initializes module + *-------------------------------------------------------------------------*/ +void environment_open(lua_State *L) { + luaL_setfuncs(L, func, 0); +} diff --git a/src/random.c b/src/random.c new file mode 100644 index 0000000..d3e1816 --- /dev/null +++ b/src/random.c @@ -0,0 +1,94 @@ +/// @submodule system +#include +#include +#include "compat.h" +#include +// #include +// #include +// #include +#include + + +// Maximum buffer size for random bytes +#define MAX_RANDOM_BUFFER_SIZE 1024 + + +/*** +Generate random bytes. +This uses `getrandom()` on Linux, `CryptGenRandom()` on Windows, and `/dev/urandom` on macOS. +@function random +@tparam[opt=1] int length number of bytes to get, must be less than or equal to `MAX_RANDOM_BUFFER_SIZE` (1024) +@treturn[1] string string of random bytes +@treturn[2] nil +@treturn[2] string error message +*/ +static int lua_get_random_bytes(lua_State* L) { + int num_bytes = luaL_optinteger(L, 1, 1); // Number of bytes, default to 1 if not provided + + if (num_bytes <= 0 || num_bytes > MAX_RANDOM_BUFFER_SIZE) { + lua_pushnil(L); + lua_pushfstring(L, "invalid number of bytes, must be between 1 and %d", MAX_RANDOM_BUFFER_SIZE); + return 2; + } + + unsigned char buffer[MAX_RANDOM_BUFFER_SIZE]; + ssize_t n; + +#ifdef _WIN32 + if (CryptGenRandom(NULL, num_bytes, buffer) == 0) { + DWORD error = GetLastError(); + lua_pushnil(L); + lua_pushfstring(L, "failed to get random data: %lu", error); + return 2; + } +#else +#ifndef __APPLE__ + // Neither Apple nor Windows + n = getrandom(buffer, num_bytes, 0); + + if (n < 0) { + lua_pushnil(L); + lua_pushstring(L, "failed to get random data"); + return 2; + } +#else + // macOS uses /dev/urandom for non-blocking + int fd = open("/dev/urandom", O_RDONLY); + if (fd < 0) { + lua_pushnil(L); + lua_pushstring(L, "failed opening /dev/urandom"); + return 2; + } + n = read(fd, buffer, num_bytes); + close(fd); + if (n < 0) { + lua_pushnil(L); + lua_pushstring(L, "failed reading /dev/urandom"); + return 2; +} + +#endif + +#endif + + lua_pushlstring(L, (const char*)buffer, num_bytes); + return 1; +} + + + +static luaL_Reg func[] = { + { "random", lua_get_random_bytes }, + { NULL, NULL } +}; + + + +/*------------------------------------------------------------------------- + * Initializes module + *-------------------------------------------------------------------------*/ +void random_open(lua_State *L) { + luaL_setfuncs(L, func, 0); + lua_pushinteger(L, MAX_RANDOM_BUFFER_SIZE); + lua_setfield(L, -2, "MAX_RANDOM_BUFFER_SIZE"); +} diff --git a/src/time.c b/src/time.c index bdceb1c..1cd81be 100644 --- a/src/time.c +++ b/src/time.c @@ -136,7 +136,7 @@ static int time_lua_monotime(lua_State *L) Sleep without a busy loop. This function will sleep, without doing a busy-loop and wasting CPU cycles. @function sleep -@tparam number seconds to sleep (fractional). +@tparam number seconds seconds to sleep (fractional). @return nothing */ #ifdef _WIN32