aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThijs Schreijer <thijs@thijsschreijer.nl>2026-02-13 14:49:50 +0100
committerThijs Schreijer <thijs@thijsschreijer.nl>2026-02-13 14:49:50 +0100
commitf6f0c77995fcd62863a7f24cbb407a68d2277759 (patch)
treee7713dd493c42404201f89993aa1513e224a53b6
parentdfd0d4b8ca3607ae39b1d2cbad4e3a7180dd6754 (diff)
downloadluasystem-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.lua133
-rw-r--r--src/random.c245
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
47end) 180end)
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). */
36static 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/***
33Generate random bytes. 88Generate random bytes.
34This uses `BCryptGenRandom()` on Windows, `getrandom()` on Linux, `arc4random_buf` on BSD, 89This 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 127static 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 138static 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) { 160Random 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).
165On 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*/
173static 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
127static luaL_Reg func[] = { 275static 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