Browse Source

Merge pull request #5266 from rspamd/vstakhov-universal-hashing-lua

[Feature] Allow to hash any Lua types
pull/5268/head
Vsevolod Stakhov 11 months ago
committed by GitHub
parent
commit
bc674074a5
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 116
      src/lua/lua_cryptobox.c
  2. 176
      test/lua/unit/hash.lua

116
src/lua/lua_cryptobox.c

@ -1362,57 +1362,110 @@ lua_cryptobox_hash_create_specific_keyed(lua_State *L)
return 1; 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; const char *data;
struct rspamd_lua_text *t; struct rspamd_lua_text *t;
gsize len; 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 = lua_newuserdata(L, sizeof(void *));
*ph = h; *ph = h;
REF_RETAIN(h); REF_RETAIN(h);
@ -1420,7 +1473,6 @@ lua_cryptobox_hash_update(lua_State *L)
return 1; return 1;
} }
/*** /***
* @method cryptobox_hash:reset() * @method cryptobox_hash:reset()
* Resets hash to the initial state * Resets hash to the initial state

176
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)
Loading…
Cancel
Save