diff options
| author | Thijs Schreijer <thijs@thijsschreijer.nl> | 2026-02-13 14:49:50 +0100 |
|---|---|---|
| committer | Thijs Schreijer <thijs@thijsschreijer.nl> | 2026-02-13 14:49:50 +0100 |
| commit | f6f0c77995fcd62863a7f24cbb407a68d2277759 (patch) | |
| tree | e7713dd493c42404201f89993aa1513e224a53b6 | |
| parent | dfd0d4b8ca3607ae39b1d2cbad4e3a7180dd6754 (diff) | |
| download | luasystem-f6f0c77995fcd62863a7f24cbb407a68d2277759.tar.gz luasystem-f6f0c77995fcd62863a7f24cbb407a68d2277759.tar.bz2 luasystem-f6f0c77995fcd62863a7f24cbb407a68d2277759.zip | |
feat(random): add rnd() to mimic math.random()
Matches Lua 5.4+ implementation. But uses crypto secure random data.
| -rw-r--r-- | spec/02-random_spec.lua | 133 | ||||
| -rw-r--r-- | src/random.c | 245 |
2 files changed, 330 insertions, 48 deletions
diff --git a/spec/02-random_spec.lua b/spec/02-random_spec.lua index 23b6d95..db5d5ce 100644 --- a/spec/02-random_spec.lua +++ b/spec/02-random_spec.lua | |||
| @@ -44,4 +44,137 @@ describe("Random:", function() | |||
| 44 | 44 | ||
| 45 | end) | 45 | end) |
| 46 | 46 | ||
| 47 | |||
| 48 | |||
| 49 | describe("rnd()", function() | ||
| 50 | |||
| 51 | local has_math_type = type(math.type) == "function" | ||
| 52 | |||
| 53 | it("with no args returns a number in [0, 1)", function() | ||
| 54 | local v, err = system.rnd() | ||
| 55 | assert.is_nil(err) | ||
| 56 | assert.is_number(v) | ||
| 57 | assert.is_true(v >= 0 and v < 1) | ||
| 58 | end) | ||
| 59 | |||
| 60 | |||
| 61 | it("with no args returns different values on multiple calls", function() | ||
| 62 | local seen = {} | ||
| 63 | for _ = 1, 20 do | ||
| 64 | local v = system.rnd() | ||
| 65 | seen[v] = true | ||
| 66 | end | ||
| 67 | local count = 0 | ||
| 68 | for _ in pairs(seen) do count = count + 1 end | ||
| 69 | assert.is_true(count >= 2, "expected at least 2 distinct values in 20 calls") | ||
| 70 | end) | ||
| 71 | |||
| 72 | |||
| 73 | it("with one arg m returns integer in [1, m]", function() | ||
| 74 | for _ = 1, 50 do | ||
| 75 | local v, err = system.rnd(6) | ||
| 76 | assert.is_nil(err) | ||
| 77 | assert.is_true(v >= 1 and v <= 6 and math.floor(v) == v) | ||
| 78 | end | ||
| 79 | end) | ||
| 80 | |||
| 81 | |||
| 82 | it("matches math.random type behaviour for one-arg m when available", function() | ||
| 83 | if not has_math_type then | ||
| 84 | return | ||
| 85 | end | ||
| 86 | -- use math.random as ground truth for numeric type | ||
| 87 | local mval = math.random(6) | ||
| 88 | local rval = system.rnd(6) | ||
| 89 | assert.are.equal(math.type(mval), math.type(rval)) | ||
| 90 | end) | ||
| 91 | |||
| 92 | |||
| 93 | it("with one arg 1 always returns 1", function() | ||
| 94 | for _ = 1, 10 do | ||
| 95 | local v, err = system.rnd(1) | ||
| 96 | assert.is_nil(err) | ||
| 97 | assert.are.equal(1, v) | ||
| 98 | end | ||
| 99 | end) | ||
| 100 | |||
| 101 | |||
| 102 | it("with one arg 0 returns a full-range integer", function() | ||
| 103 | local v, err = system.rnd(0) | ||
| 104 | assert.is_nil(err) | ||
| 105 | assert.is_true(type(v) == "number" or (math.type and math.type(v) == "integer")) | ||
| 106 | assert.is_true(v >= -0x8000000000000000 and v <= 0x7fffffffffffffff) | ||
| 107 | end) | ||
| 108 | |||
| 109 | |||
| 110 | it("matches math.random type behaviour for arg 0 when math.random(0) is supported", function() | ||
| 111 | if not has_math_type then | ||
| 112 | return | ||
| 113 | end | ||
| 114 | local ok, mval = pcall(math.random, 0) | ||
| 115 | if not ok then | ||
| 116 | return -- older Lua where math.random(0) is not supported | ||
| 117 | end | ||
| 118 | local rval = system.rnd(0) | ||
| 119 | assert.are.equal(math.type(mval), math.type(rval)) | ||
| 120 | end) | ||
| 121 | |||
| 122 | |||
| 123 | it("with two args returns integer in [m, n]", function() | ||
| 124 | for _ = 1, 30 do | ||
| 125 | local v, err = system.rnd(10, 20) | ||
| 126 | assert.is_nil(err) | ||
| 127 | assert.is_true(v >= 10 and v <= 20 and math.floor(v) == v) | ||
| 128 | end | ||
| 129 | end) | ||
| 130 | |||
| 131 | |||
| 132 | it("matches math.random type behaviour for two-arg range when available", function() | ||
| 133 | if not has_math_type then | ||
| 134 | return | ||
| 135 | end | ||
| 136 | local mval = math.random(10, 20) | ||
| 137 | local rval = system.rnd(10, 20) | ||
| 138 | assert.are.equal(math.type(mval), math.type(rval)) | ||
| 139 | end) | ||
| 140 | |||
| 141 | |||
| 142 | it("with two args supports negative range", function() | ||
| 143 | for _ = 1, 30 do | ||
| 144 | local v, err = system.rnd(-5, 5) | ||
| 145 | assert.is_nil(err) | ||
| 146 | assert.is_true(v >= -5 and v <= 5 and math.floor(v) == v) | ||
| 147 | end | ||
| 148 | end) | ||
| 149 | |||
| 150 | |||
| 151 | it("with two equal args returns that value", function() | ||
| 152 | local v, err = system.rnd(7, 7) | ||
| 153 | assert.is_nil(err) | ||
| 154 | assert.are.equal(7, v) | ||
| 155 | end) | ||
| 156 | |||
| 157 | |||
| 158 | it("returns nil and error for empty interval (m > n)", function() | ||
| 159 | local v, err = system.rnd(10, 5) | ||
| 160 | assert.is_falsy(v) | ||
| 161 | assert.are.equal("interval is empty", err) | ||
| 162 | end) | ||
| 163 | |||
| 164 | |||
| 165 | it("returns nil and error for invalid one-arg (m < 1, m ~= 0)", function() | ||
| 166 | local v, err = system.rnd(-1) | ||
| 167 | assert.is_falsy(v) | ||
| 168 | assert.are.equal("interval is empty", err) | ||
| 169 | end) | ||
| 170 | |||
| 171 | |||
| 172 | it("returns nil and error for wrong number of arguments", function() | ||
| 173 | local v, err = system.rnd(1, 2, 3) | ||
| 174 | assert.is_falsy(v) | ||
| 175 | assert.are.equal("wrong number of arguments", err) | ||
| 176 | end) | ||
| 177 | |||
| 178 | end) | ||
| 179 | |||
| 47 | end) | 180 | end) |
diff --git a/src/random.c b/src/random.c index 4c92745..6b09a55 100644 --- a/src/random.c +++ b/src/random.c | |||
| @@ -8,6 +8,8 @@ | |||
| 8 | #include <lauxlib.h> | 8 | #include <lauxlib.h> |
| 9 | #include "compat.h" | 9 | #include "compat.h" |
| 10 | #include <fcntl.h> | 10 | #include <fcntl.h> |
| 11 | #include <stdint.h> | ||
| 12 | #include <stddef.h> | ||
| 11 | 13 | ||
| 12 | #ifdef _WIN32 | 14 | #ifdef _WIN32 |
| 13 | #include <windows.h> | 15 | #include <windows.h> |
| @@ -29,6 +31,59 @@ | |||
| 29 | #endif | 31 | #endif |
| 30 | 32 | ||
| 31 | 33 | ||
| 34 | /* Fill buffer with n cryptographically secure random bytes. Returns 0 on success; | ||
| 35 | * on failure pushes nil and error message and returns 2 (caller should return 2). */ | ||
| 36 | static int fill_random_bytes(lua_State *L, unsigned char *buffer, size_t n) { | ||
| 37 | size_t total_read = 0; | ||
| 38 | |||
| 39 | #ifdef _WIN32 | ||
| 40 | if (!BCRYPT_SUCCESS(BCryptGenRandom(NULL, buffer, (ULONG)n, BCRYPT_USE_SYSTEM_PREFERRED_RNG))) { | ||
| 41 | lua_pushnil(L); | ||
| 42 | lua_pushfstring(L, "failed to get random data: %lu", (unsigned long)GetLastError()); | ||
| 43 | return 2; | ||
| 44 | } | ||
| 45 | (void)total_read; | ||
| 46 | |||
| 47 | #elif defined(__linux__) && !defined(USE_DEV_URANDOM) | ||
| 48 | while (total_read < n) { | ||
| 49 | ssize_t got = getrandom(buffer + total_read, n - total_read, 0); | ||
| 50 | if (got < 0) { | ||
| 51 | if (errno == EINTR) continue; | ||
| 52 | lua_pushnil(L); | ||
| 53 | lua_pushfstring(L, "getrandom() failed: %s", strerror(errno)); | ||
| 54 | return 2; | ||
| 55 | } | ||
| 56 | total_read += (size_t)got; | ||
| 57 | } | ||
| 58 | |||
| 59 | #elif defined(__APPLE__) || (defined(__unix__) && !defined(USE_DEV_URANDOM)) | ||
| 60 | arc4random_buf(buffer, n); | ||
| 61 | |||
| 62 | #else | ||
| 63 | int fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC); | ||
| 64 | if (fd < 0) { | ||
| 65 | lua_pushnil(L); | ||
| 66 | lua_pushstring(L, "failed opening /dev/urandom"); | ||
| 67 | return 2; | ||
| 68 | } | ||
| 69 | while (total_read < n) { | ||
| 70 | ssize_t got = read(fd, buffer + total_read, n - total_read); | ||
| 71 | if (got < 0) { | ||
| 72 | if (errno == EINTR) continue; | ||
| 73 | lua_pushnil(L); | ||
| 74 | lua_pushfstring(L, "failed reading /dev/urandom: %s", strerror(errno)); | ||
| 75 | close(fd); | ||
| 76 | return 2; | ||
| 77 | } | ||
| 78 | total_read += (size_t)got; | ||
| 79 | } | ||
| 80 | close(fd); | ||
| 81 | #endif | ||
| 82 | |||
| 83 | return 0; | ||
| 84 | } | ||
| 85 | |||
| 86 | |||
| 32 | /*** | 87 | /*** |
| 33 | Generate random bytes. | 88 | Generate random bytes. |
| 34 | This uses `BCryptGenRandom()` on Windows, `getrandom()` on Linux, `arc4random_buf` on BSD, | 89 | This uses `BCryptGenRandom()` on Windows, `getrandom()` on Linux, `arc4random_buf` on BSD, |
| @@ -60,72 +115,166 @@ static int lua_get_random_bytes(lua_State* L) { | |||
| 60 | return 2; | 115 | return 2; |
| 61 | } | 116 | } |
| 62 | 117 | ||
| 63 | ssize_t total_read = 0; | 118 | int ret = fill_random_bytes(L, buffer, (size_t)num_bytes); |
| 119 | if (ret != 0) return ret; | ||
| 64 | 120 | ||
| 65 | #ifdef _WIN32 | 121 | lua_pushlstring(L, (const char*)buffer, num_bytes); |
| 66 | // Use BCryptGenRandom() on Windows | 122 | return 1; |
| 67 | if (!BCRYPT_SUCCESS(BCryptGenRandom(NULL, buffer, num_bytes, BCRYPT_USE_SYSTEM_PREFERRED_RNG))) { | 123 | } |
| 68 | DWORD error = GetLastError(); | ||
| 69 | lua_pushnil(L); | ||
| 70 | lua_pushfstring(L, "failed to get random data: %lu", error); | ||
| 71 | return 2; | ||
| 72 | } | ||
| 73 | 124 | ||
| 74 | #elif defined(__linux__) && !defined(USE_DEV_URANDOM) | ||
| 75 | // Use getrandom() on Linux | ||
| 76 | while (total_read < num_bytes) { | ||
| 77 | ssize_t n = getrandom(buffer + total_read, num_bytes - total_read, 0); | ||
| 78 | if (n < 0) { | ||
| 79 | if (errno == EINTR) continue; // Retry on interrupt | ||
| 80 | lua_pushnil(L); | ||
| 81 | lua_pushfstring(L, "getrandom() failed: %s", strerror(errno)); | ||
| 82 | return 2; | ||
| 83 | } | ||
| 84 | total_read += n; | ||
| 85 | } | ||
| 86 | 125 | ||
| 87 | #elif defined(__APPLE__) || (defined(__unix__) && !defined(USE_DEV_URANDOM)) | 126 | /* Read 8 bytes into *out; return 0 on success, 2 on error (nil + msg pushed). */ |
| 88 | // Use arc4random_buf() on BSD/macOS | 127 | static int read_u64(lua_State *L, uint64_t *out) { |
| 89 | arc4random_buf(buffer, num_bytes); | 128 | unsigned char buf[8]; |
| 129 | int ret = fill_random_bytes(L, buf, 8); | ||
| 130 | if (ret != 0) return ret; | ||
| 131 | *out = (uint64_t)buf[0] | ((uint64_t)buf[1] << 8) | ((uint64_t)buf[2] << 16) | | ||
| 132 | ((uint64_t)buf[3] << 24) | ((uint64_t)buf[4] << 32) | ((uint64_t)buf[5] << 40) | | ||
| 133 | ((uint64_t)buf[6] << 48) | ((uint64_t)buf[7] << 56); | ||
| 134 | return 0; | ||
| 135 | } | ||
| 90 | 136 | ||
| 91 | #else | 137 | /* Project uniform random in [0, n] using rejection (Mersenne-style). *out is in [0, n]. */ |
| 92 | // fall back to /dev/urandom for anything else | 138 | static int project_u64(lua_State *L, uint64_t ran, uint64_t n, uint64_t *out) { |
| 93 | int fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC); | 139 | if ((n & (n + 1)) == 0) { |
| 94 | if (fd < 0) { | 140 | *out = ran & n; |
| 95 | lua_pushnil(L); | 141 | return 0; |
| 96 | lua_pushstring(L, "failed opening /dev/urandom"); | 142 | } |
| 97 | return 2; | 143 | uint64_t lim = n; |
| 144 | lim |= (lim >> 1); | ||
| 145 | lim |= (lim >> 2); | ||
| 146 | lim |= (lim >> 4); | ||
| 147 | lim |= (lim >> 8); | ||
| 148 | lim |= (lim >> 16); | ||
| 149 | lim |= (lim >> 32); | ||
| 150 | while ((ran &= lim) > n) { | ||
| 151 | int ret = read_u64(L, &ran); | ||
| 152 | if (ret != 0) return ret; | ||
| 98 | } | 153 | } |
| 154 | *out = ran; | ||
| 155 | return 0; | ||
| 156 | } | ||
| 99 | 157 | ||
| 100 | while (total_read < num_bytes) { | ||
| 101 | ssize_t n = read(fd, buffer + total_read, num_bytes - total_read); | ||
| 102 | 158 | ||
| 103 | if (n < 0) { | 159 | /*** |
| 104 | if (errno == EINTR) { | 160 | Random number mimicking Lua 5.4 math.random, using crypto-secure bytes. |
| 105 | continue; // Interrupted, retry | 161 | - No args: returns a float in [0, 1). |
| 162 | - One arg 0: returns a full-range random integer (whole lua_Integer range). | ||
| 163 | - One arg m (m >= 1): returns an integer in [1, m] (inclusive). | ||
| 164 | - Two args m, n: returns an integer in [m, n] (inclusive). | ||
| 165 | On invalid arguments returns nil and an error message. | ||
| 166 | @function rnd | ||
| 167 | @tparam[opt] int m upper bound (or 0 for full-range), or lower bound when used with n | ||
| 168 | @tparam[opt] int n upper bound (when used with m) | ||
| 169 | @treturn[1] number|int float in [0,1) or integer in the requested range | ||
| 170 | @treturn[2] nil | ||
| 171 | @treturn[2] string error message | ||
| 172 | */ | ||
| 173 | static int lua_rnd(lua_State *L) { | ||
| 174 | int nargs = lua_gettop(L); | ||
| 175 | unsigned char buf[8]; | ||
| 176 | uint64_t u; | ||
| 106 | 177 | ||
| 107 | } else { | 178 | if (nargs == 0) { |
| 108 | lua_pushnil(L); | 179 | int ret = fill_random_bytes(L, buf, 8); |
| 109 | lua_pushfstring(L, "failed reading /dev/urandom: %s", strerror(errno)); | 180 | if (ret != 0) return ret; |
| 110 | close(fd); | 181 | u = (uint64_t)buf[0] | ((uint64_t)buf[1] << 8) | ((uint64_t)buf[2] << 16) | |
| 111 | return 2; | 182 | ((uint64_t)buf[3] << 24) | ((uint64_t)buf[4] << 32) | ((uint64_t)buf[5] << 40) | |
| 112 | } | 183 | ((uint64_t)buf[6] << 48) | ((uint64_t)buf[7] << 56); |
| 113 | } | 184 | /* 53 bits for double [0, 1) */ |
| 185 | lua_pushnumber(L, (lua_Number)((u >> 11) * (1.0 / ((uint64_t)1 << 53)))); | ||
| 186 | return 1; | ||
| 187 | } | ||
| 188 | |||
| 189 | #if LUA_VERSION_NUM >= 503 | ||
| 190 | #define RND_INT lua_Integer | ||
| 191 | #define RND_CHECKINT(L, i) luaL_checkinteger(L, (i)) | ||
| 192 | #define RND_MAXINTEGER LUA_MAXINTEGER | ||
| 193 | #define RND_MININTEGER LUA_MININTEGER | ||
| 194 | #else | ||
| 195 | #define RND_INT long long | ||
| 196 | #define RND_CHECKINT(L, i) ((long long)luaL_checknumber(L, (i))) | ||
| 197 | #endif | ||
| 114 | 198 | ||
| 115 | total_read += n; | 199 | if (nargs == 1) { |
| 200 | RND_INT m = RND_CHECKINT(L, 1); | ||
| 201 | if (m == 0) { | ||
| 202 | /* full-range random integer */ | ||
| 203 | int ret = read_u64(L, &u); | ||
| 204 | if (ret != 0) return ret; | ||
| 205 | #if LUA_VERSION_NUM >= 503 | ||
| 206 | lua_pushinteger(L, (lua_Integer)(int64_t)u); | ||
| 207 | #else | ||
| 208 | lua_pushnumber(L, (lua_Number)(long long)(int64_t)u); | ||
| 209 | #endif | ||
| 210 | return 1; | ||
| 211 | } | ||
| 212 | if (m < 1) { | ||
| 213 | lua_pushnil(L); | ||
| 214 | lua_pushstring(L, "interval is empty"); | ||
| 215 | return 2; | ||
| 216 | } | ||
| 217 | /* [1, m] -> range size m, values 0..m-1 then +1 */ | ||
| 218 | { | ||
| 219 | uint64_t r; | ||
| 220 | int ret = read_u64(L, &u); | ||
| 221 | if (ret != 0) return ret; | ||
| 222 | ret = project_u64(L, u, (uint64_t)(m - 1), &r); | ||
| 223 | if (ret != 0) return ret; | ||
| 224 | #if LUA_VERSION_NUM >= 503 | ||
| 225 | lua_pushinteger(L, (lua_Integer)r + 1); | ||
| 226 | #else | ||
| 227 | lua_pushnumber(L, (lua_Number)((long long)r + 1)); | ||
| 228 | #endif | ||
| 229 | return 1; | ||
| 230 | } | ||
| 116 | } | 231 | } |
| 117 | 232 | ||
| 118 | close(fd); | 233 | if (nargs == 2) { |
| 234 | RND_INT low = RND_CHECKINT(L, 1); | ||
| 235 | RND_INT up = RND_CHECKINT(L, 2); | ||
| 236 | if (low > up) { | ||
| 237 | lua_pushnil(L); | ||
| 238 | lua_pushstring(L, "interval is empty"); | ||
| 239 | return 2; | ||
| 240 | } | ||
| 241 | #if LUA_VERSION_NUM >= 503 | ||
| 242 | if (low < 0 && up > 0 && (lua_Unsigned)(up - low) > (lua_Unsigned)LUA_MAXINTEGER) { | ||
| 243 | lua_pushnil(L); | ||
| 244 | lua_pushstring(L, "interval too large"); | ||
| 245 | return 2; | ||
| 246 | } | ||
| 247 | #endif | ||
| 248 | { | ||
| 249 | uint64_t range = (uint64_t)(up - low) + 1; | ||
| 250 | uint64_t r; | ||
| 251 | int ret = read_u64(L, &u); | ||
| 252 | if (ret != 0) return ret; | ||
| 253 | ret = project_u64(L, u, range - 1, &r); | ||
| 254 | if (ret != 0) return ret; | ||
| 255 | #if LUA_VERSION_NUM >= 503 | ||
| 256 | lua_pushinteger(L, (lua_Integer)((lua_Unsigned)low + (lua_Unsigned)r)); | ||
| 257 | #else | ||
| 258 | lua_pushnumber(L, (lua_Number)((long long)low + (long long)r)); | ||
| 119 | #endif | 259 | #endif |
| 260 | return 1; | ||
| 261 | } | ||
| 262 | } | ||
| 120 | 263 | ||
| 121 | lua_pushlstring(L, (const char*)buffer, num_bytes); | 264 | lua_pushnil(L); |
| 122 | return 1; | 265 | lua_pushliteral(L, "wrong number of arguments"); |
| 123 | } | 266 | return 2; |
| 124 | 267 | ||
| 268 | #undef RND_INT | ||
| 269 | #undef RND_CHECKINT | ||
| 270 | #undef RND_MAXINTEGER | ||
| 271 | #undef RND_MININTEGER | ||
| 272 | } | ||
| 125 | 273 | ||
| 126 | 274 | ||
| 127 | static luaL_Reg func[] = { | 275 | static luaL_Reg func[] = { |
| 128 | { "random", lua_get_random_bytes }, | 276 | { "random", lua_get_random_bytes }, |
| 277 | { "rnd", lua_rnd }, | ||
| 129 | { NULL, NULL } | 278 | { NULL, NULL } |
| 130 | }; | 279 | }; |
| 131 | 280 | ||
