diff options
author | Vsevolod Stakhov <vsevolod@rspamd.com> | 2024-12-20 17:19:12 +0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-12-20 17:19:12 +0600 |
commit | bc674074a56e77061c3d97d12c2784e0b8d96231 (patch) | |
tree | 2a98a8eb558f27278fce6694bd7a6ac406e9d035 | |
parent | d35385f951ee38dfdd0bedc77eb7e2d1e5809e40 (diff) | |
parent | 223d286c5434de74b7ed05c0ace2381174185452 (diff) | |
download | rspamd-bc674074a56e77061c3d97d12c2784e0b8d96231.tar.gz rspamd-bc674074a56e77061c3d97d12c2784e0b8d96231.zip |
Merge pull request #5266 from rspamd/vstakhov-universal-hashing-lua
[Feature] Allow to hash any Lua types
-rw-r--r-- | src/lua/lua_cryptobox.c | 116 | ||||
-rw-r--r-- | test/lua/unit/hash.lua | 176 |
2 files changed, 260 insertions, 32 deletions
diff --git a/src/lua/lua_cryptobox.c b/src/lua/lua_cryptobox.c index 9600a4732..b562c4778 100644 --- a/src/lua/lua_cryptobox.c +++ b/src/lua/lua_cryptobox.c @@ -1362,57 +1362,110 @@ lua_cryptobox_hash_create_specific_keyed(lua_State *L) return 1; } -/*** - * @method cryptobox_hash:update(data) - * Updates hash with the specified data (hash should not be finalized using `hex` or `bin` methods) - * @param {string} data data to hash - */ -static int -lua_cryptobox_hash_update(lua_State *L) +static void +lua_cryptobox_update_pos(lua_State *L, struct rspamd_lua_cryptobox_hash *h, int pos) { - LUA_TRACE_POINT; - struct rspamd_lua_cryptobox_hash *h = lua_check_cryptobox_hash(L, 1), **ph; const char *data; struct rspamd_lua_text *t; gsize len; - if (lua_isuserdata(L, 2)) { - t = lua_check_text(L, 2); + /* Inverse pos if it is relative to the top of the stack */ + if (pos < 0) { + pos = lua_gettop(L) + pos + 1; + } - if (!t) { - return luaL_error(L, "invalid arguments"); + switch (lua_type(L, pos)) { + case LUA_TSTRING: + data = lua_tolstring(L, pos, &len); + rspamd_lua_hash_update(h, data, len); + break; + + case LUA_TNUMBER: { + lua_Number n = lua_tonumber(L, pos); + if (n == (lua_Number) (lua_Integer) n) { + lua_Integer i = lua_tointeger(L, pos); + rspamd_lua_hash_update(h, (void *) &i, sizeof(i)); } + else { - data = t->start; - len = t->len; + rspamd_lua_hash_update(h, (void *) &n, sizeof(n)); + } + + break; } - else { - data = luaL_checklstring(L, 2, &len); + + case LUA_TBOOLEAN: { + char b = lua_toboolean(L, pos); + rspamd_lua_hash_update(h, &b, sizeof(b)); + break; } - if (lua_isnumber(L, 3)) { - gsize nlen = lua_tonumber(L, 3); + case LUA_TTABLE: { - if (nlen > len) { - return luaL_error(L, "invalid length: %d while %d is available", - (int) nlen, (int) len); + /* Hash array part */ + gsize alen; +#if LUA_VERSION_NUM >= 502 + alen = lua_rawlen(L, 2); +#else + alen = lua_objlen(L, pos); +#endif + + for (gsize i = 1; i <= alen; i++) { + lua_rawgeti(L, pos, i); + lua_cryptobox_update_pos(L, h, -1); /* Recurse */ + lua_pop(L, 1); } - len = nlen; - } + /* Hash key-value pairs */ + lua_pushnil(L); + while (lua_next(L, pos) != 0) { + /* Hash key */ + lua_pushvalue(L, -2); + lua_cryptobox_update_pos(L, h, -1); + lua_pop(L, 1); - if (h && data) { - if (!h->is_finished) { - rspamd_lua_hash_update(h, data, len); + /* Hash value */ + lua_cryptobox_update_pos(L, h, -1); + lua_pop(L, 1); } - else { - return luaL_error(L, "hash is already finalized"); + + break; + } + + case LUA_TUSERDATA: + t = lua_check_text(L, 2); + if (t) { + rspamd_lua_hash_update(h, t->start, t->len); } + break; + + case LUA_TFUNCTION: + case LUA_TTHREAD: + case LUA_TNIL: + default: + /* Skip these types */ + break; } - else { - return luaL_error(L, "invalid arguments"); +} + +/*** + * @method cryptobox_hash:update(data) + * Updates hash with the specified data (hash should not be finalized using `hex` or `bin` methods) + * @param {string} data data to hash + */ +static int +lua_cryptobox_hash_update(lua_State *L) +{ + LUA_TRACE_POINT; + struct rspamd_lua_cryptobox_hash *h = lua_check_cryptobox_hash(L, 1), **ph; + + + if (h == NULL || h->is_finished) { + return luaL_error(L, "invalid arguments or hash is already finalized"); } + lua_cryptobox_update_pos(L, h, 2); + ph = lua_newuserdata(L, sizeof(void *)); *ph = h; REF_RETAIN(h); @@ -1420,7 +1473,6 @@ lua_cryptobox_hash_update(lua_State *L) return 1; } - /*** * @method cryptobox_hash:reset() * Resets hash to the initial state diff --git a/test/lua/unit/hash.lua b/test/lua/unit/hash.lua new file mode 100644 index 000000000..4e632e8a1 --- /dev/null +++ b/test/lua/unit/hash.lua @@ -0,0 +1,176 @@ +local hash = require 'rspamd_cryptobox_hash' + +context("Cryptobox hash tests", function() + + local function hash_value(value) + local h = hash.create() + h:update(value) + return h:hex() + end + + local function compare_hashes(val1, val2) + return hash_value(val1) == hash_value(val2) + end + + context("Basic type hashing", function() + test("Handles strings", function() + local h1 = hash_value("test") + local h2 = hash_value("test") + assert_equal(h1, h2, "Same strings should hash to same value") + + assert_not_equal(hash_value("test"), hash_value("test2"), + "Different strings should hash differently") + end) + + test("Handles numbers", function() + -- Integer tests + assert_equal(hash_value(123), hash_value(123)) + assert_not_equal(hash_value(123), hash_value(124)) + + -- Float tests + assert_equal(hash_value(123.45), hash_value(123.45)) + assert_not_equal(hash_value(123.45), hash_value(123.46)) + + -- Different number types should hash differently + assert_not_equal(hash_value(123), hash_value(123.1)) + end) + + test("Handles booleans", function() + assert_equal(hash_value(true), hash_value(true)) + assert_equal(hash_value(false), hash_value(false)) + assert_not_equal(hash_value(true), hash_value(false)) + end) + + test("Handles nil", function() + local h1 = hash.create() + local h2 = hash.create() + h1:update(nil) + h2:update(nil) + assert_equal(h1:hex(), h2:hex()) + end) + end) + + context("Table hashing", function() + test("Handles array tables", function() + assert_equal(hash_value({ 1, 2, 3 }), hash_value({ 1, 2, 3 })) + assert_not_equal(hash_value({ 1, 2, 3 }), hash_value({ 1, 2, 4 })) + assert_not_equal(hash_value({ 1, 2, 3 }), hash_value({ 1, 2 })) + end) + + test("Handles key-value tables", function() + assert_equal( + hash_value({ foo = "bar", baz = 123 }), + hash_value({ foo = "bar", baz = 123 }) + ) + assert_not_equal( + hash_value({ foo = "bar" }), + hash_value({ foo = "baz" }) + ) + end) + + test("Handles mixed tables", function() + assert_equal( + hash_value({ 1, 2, foo = "bar" }), + hash_value({ 1, 2, foo = "bar" }) + ) + assert_not_equal( + hash_value({ 1, 2, foo = "bar" }), + hash_value({ 1, 2, foo = "baz" }) + ) + end) + + test("Handles nested tables", function() + assert_equal( + hash_value({ 1, { 2, 3 }, foo = { bar = "baz" } }), + hash_value({ 1, { 2, 3 }, foo = { bar = "baz" } }) + ) + assert_not_equal( + hash_value({ 1, { 2, 3 } }), + hash_value({ 1, { 2, 4 } }) + ) + end) + end) + + context("Complex scenarios", function() + test("Handles multiple updates", function() + local h1 = hash.create() + h1:update("test") + h1:update(123) + h1:update({ foo = "bar" }) + + local h2 = hash.create() + h2:update("test") + h2:update(123) + h2:update({ foo = "bar" }) + + assert_equal(h1:hex(), h2:hex()) + end) + + test("Order matters for updates", function() + local h1 = hash.create() + h1:update("a") + h1:update("b") + + local h2 = hash.create() + h2:update("b") + h2:update("a") + + assert_not_equal(h1:hex(), h2:hex()) + end) + + test("Handles all types together", function() + local complex = { + str = "test", + num = 123, + float = 123.45, + bool = true, + arr = { 1, 2, 3 }, + nested = { + foo = { + bar = "baz" + } + } + } + + assert_equal(hash_value(complex), hash_value(complex)) + end) + end) + + context("Error conditions", function() + test("Prevents update after finalization", function() + local h = hash.create() + h:update("test") + local _ = h:hex() -- finalize + assert_error(function() + h:update("more") + end) + end) + + test("Handles function values", function() + local h = hash.create() + local f = function() + end + assert_not_error(function() + h:update(f) + end) + end) + end) + + context("Determinism tests", function() + test("Same input always produces same hash", function() + local inputs = { + "test string", + 123, + true, + { 1, 2, 3 }, + { foo = "bar", nested = { 1, 2, 3 } }, + } + + for _, input in ipairs(inputs) do + local h1 = hash_value(input) + local h2 = hash_value(input) + assert_equal(h1, h2, "Hash should be deterministic for: " .. type(input)) + end + end) + end) +end)
\ No newline at end of file |