aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorVsevolod Stakhov <vsevolod@rspamd.com>2024-12-20 17:19:12 +0600
committerGitHub <noreply@github.com>2024-12-20 17:19:12 +0600
commitbc674074a56e77061c3d97d12c2784e0b8d96231 (patch)
tree2a98a8eb558f27278fce6694bd7a6ac406e9d035
parentd35385f951ee38dfdd0bedc77eb7e2d1e5809e40 (diff)
parent223d286c5434de74b7ed05c0ace2381174185452 (diff)
downloadrspamd-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.c116
-rw-r--r--test/lua/unit/hash.lua176
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