You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
255 lines
6.9 KiB
Lua
255 lines
6.9 KiB
Lua
-- 数字编码库
|
|
-- https://github.com/leihog/hashids.lua
|
|
local ceil = math.ceil
|
|
local floor = math.floor
|
|
local pow = math.pow
|
|
local substr = string.sub
|
|
local upcase = string.upper
|
|
local format = string.format
|
|
local strcat = table.concat
|
|
local push = table.insert
|
|
local unpack = table.unpack or unpack
|
|
|
|
local str_switch_pos = function(str, pos1, pos2)
|
|
pos1 = pos1 + 1;
|
|
pos2 = pos2 + 1;
|
|
local a, b = str:sub(pos1, pos1), str:sub(pos2, pos2);
|
|
if pos1 > pos2 then
|
|
return str:gsub(a, b, 1):gsub(b, a, 1);
|
|
end
|
|
return str:gsub(b, a, 1):gsub(a, b, 1);
|
|
end
|
|
|
|
local hash_mt = {};
|
|
hash_mt.__index = hash_mt;
|
|
|
|
local function gcap(str, pos)
|
|
pos = pos + 1;
|
|
return str:sub(pos, pos);
|
|
end
|
|
|
|
-- TODO using string concatenation with .. might not be the fastest in a loop
|
|
local function hash(number, alphabet)
|
|
local hash, alen = "", alphabet:len();
|
|
repeat
|
|
hash = gcap(alphabet, (number % alen)) .. hash;
|
|
number = floor(number / alen);
|
|
until number == 0
|
|
|
|
return hash;
|
|
end
|
|
|
|
local function unhash(input, alphabet)
|
|
local number, ilen, alen = 0, input:len(), alphabet:len();
|
|
|
|
for i = 0, ilen do
|
|
local cpos = (alphabet:find(gcap(input, i), 1, true) - 1);
|
|
number = number + cpos * pow(alen, (ilen - i - 1))
|
|
end
|
|
|
|
return number;
|
|
end
|
|
|
|
local function consistent_shuffle(alphabet, salt)
|
|
local slen = salt:len();
|
|
if slen == 0 then
|
|
return alphabet
|
|
end
|
|
|
|
local v, p = 0, 0;
|
|
for i = (alphabet:len() - 1), 1, -1 do
|
|
v = (v % slen);
|
|
local ord = gcap(salt, v):byte();
|
|
p = p + ord;
|
|
local j = (ord + v + p) % i;
|
|
|
|
alphabet = str_switch_pos(alphabet, j, i);
|
|
v = v + 1;
|
|
end
|
|
|
|
return alphabet;
|
|
end
|
|
|
|
function hash_mt:encode(...)
|
|
local numbers = {select(1, ...)};
|
|
if #numbers == 0 then
|
|
return ""
|
|
end
|
|
local numbers_size, hash_int = #numbers, 0;
|
|
|
|
for i, number in ipairs(numbers) do
|
|
assert(type(number) == 'number', "all paramters must be numbers");
|
|
hash_int = hash_int + (number % ((i - 1) + 100));
|
|
end
|
|
|
|
local alpha = self.alphabet;
|
|
local alpha_len = alpha:len();
|
|
|
|
local lottery = gcap(alpha, hash_int % alpha_len);
|
|
local ret = lottery;
|
|
local last = nil;
|
|
|
|
for i, number in ipairs(numbers) do
|
|
-- for i=1, #numbers do
|
|
-- local number = numbers[i];
|
|
alpha = consistent_shuffle(alpha, substr(strcat({lottery, self.salt, alpha}), 1, alpha_len));
|
|
last = hash(number, alpha);
|
|
ret = ret .. last;
|
|
|
|
if i < numbers_size then
|
|
number = number % (last:byte() + (i - 1));
|
|
ret = ret .. gcap(self.seps, (number % self.seps:len()));
|
|
end
|
|
end
|
|
|
|
local guards_len = self.guards:len();
|
|
if ret:len() < self.min_hash_length then
|
|
local guard_index = (hash_int + gcap(ret, 0):byte()) % guards_len;
|
|
ret = gcap(self.guards, guard_index) .. ret;
|
|
|
|
if ret:len() < self.min_hash_length then
|
|
guard_index = (hash_int + gcap(ret, 2):byte()) % guards_len;
|
|
ret = ret .. gcap(self.guards, guard_index);
|
|
end
|
|
end
|
|
|
|
local half_len, excess = floor(alpha_len * 0.5), 0; -- alpha_len / 2
|
|
while ret:len() < self.min_hash_length do
|
|
alpha = consistent_shuffle(alpha, alpha);
|
|
ret = alpha:sub(half_len + 1) .. ret .. alpha:sub(1, half_len);
|
|
|
|
excess = (ret:len() - self.min_hash_length);
|
|
if excess > 0 then
|
|
excess = (excess * 0.5);
|
|
ret = ret:sub(floor(excess + 1), floor(excess + self.min_hash_length));
|
|
end
|
|
end
|
|
|
|
return ret;
|
|
end
|
|
|
|
function hash_mt:encode_hex(str)
|
|
if str:match("%X") then
|
|
return ""
|
|
end
|
|
local pos, max, numbers = 0, #str, {}
|
|
while true do
|
|
local part = substr(str, pos + 1, pos + 12)
|
|
if part == "" then
|
|
break
|
|
end
|
|
pos = pos + #part
|
|
push(numbers, tonumber("1" .. part, 16))
|
|
end
|
|
|
|
return self:encode(unpack(numbers))
|
|
end
|
|
|
|
function hash_mt:decode(hash)
|
|
-- TODO validate input
|
|
|
|
local parts, index = {}, 1;
|
|
for part in hash:gmatch("[^" .. self.guards .. "]+") do
|
|
parts[index] = part;
|
|
index = index + 1;
|
|
end
|
|
|
|
local num_parts, t, lottery = #parts;
|
|
if num_parts == 3 or num_parts == 2 then
|
|
t = parts[2];
|
|
else
|
|
t = parts[1];
|
|
end
|
|
|
|
lottery = gcap(t, 0); -- put the first char in lottery
|
|
t = t:sub(2); -- then put the rest in t
|
|
|
|
parts, index = {}, 1;
|
|
for part in t:gmatch("[^" .. self.seps .. "]+") do
|
|
parts[index] = part;
|
|
index = index + 1;
|
|
end
|
|
|
|
local ret, alpha = {}, self.alphabet;
|
|
for i = 1, #parts do
|
|
alpha = consistent_shuffle(alpha, substr(strcat({lottery, self.salt, alpha}), 1, self.alphabet_length));
|
|
ret[i] = unhash(parts[i], alpha);
|
|
end
|
|
|
|
return ret;
|
|
end
|
|
|
|
function hash_mt:decode_hex(hash)
|
|
local result, numbers = {}, self:decode(hash)
|
|
for _, number in ipairs(numbers) do
|
|
push(result, substr(format("%x", number), 2))
|
|
end
|
|
|
|
return upcase(strcat(result))
|
|
end
|
|
|
|
return {
|
|
VERSION = "1.0.6",
|
|
new = function(salt, min_hash_length, alphabet)
|
|
salt = salt or "";
|
|
min_hash_length = min_hash_length or 0;
|
|
alphabet = alphabet or "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
|
|
-- TODO make sure alphabet doesn't contain duplicates.
|
|
|
|
local tmp_seps, tmp_alpha, c = "", "";
|
|
local seps = "cfhistuCFHISTU";
|
|
|
|
for i = 1, alphabet:len() do
|
|
c = alphabet:sub(i, i);
|
|
|
|
if seps:find(c, 1, true) then
|
|
tmp_seps = tmp_seps .. c;
|
|
else
|
|
tmp_alpha = tmp_alpha .. c;
|
|
end
|
|
end
|
|
|
|
seps = consistent_shuffle(tmp_seps, salt);
|
|
alphabet = tmp_alpha;
|
|
|
|
-- constants
|
|
local SEPS_DIV = 3.5;
|
|
local GUARD_DIV = 12;
|
|
|
|
if seps:len() == 0 or (alphabet:len() / seps:len()) > SEPS_DIV then
|
|
local seps_len = floor(ceil(alphabet:len() / SEPS_DIV));
|
|
if seps_len == 1 then
|
|
seps_len = 2
|
|
end
|
|
|
|
if seps_len > seps:len() then
|
|
local diff = seps_len - seps:len();
|
|
seps = seps .. alphabet:sub(1, diff);
|
|
alphabet = alphabet:sub(diff + 1);
|
|
else
|
|
seps = seps:sub(1, seps_len);
|
|
end
|
|
end
|
|
|
|
alphabet = consistent_shuffle(alphabet, salt);
|
|
local guards = "";
|
|
local guard_count = ceil(alphabet:len() / GUARD_DIV);
|
|
if alphabet:len() < 3 then
|
|
guards = seps:sub(1, guard_count);
|
|
seps = seps:sub(guard_count + 1);
|
|
else
|
|
guards = alphabet:sub(1, guard_count);
|
|
alphabet = alphabet:sub(guard_count + 1);
|
|
end
|
|
|
|
local obj = {
|
|
salt = salt,
|
|
alphabet = alphabet,
|
|
seps = seps,
|
|
guards = guards,
|
|
min_hash_length = min_hash_length,
|
|
};
|
|
return setmetatable(obj, hash_mt);
|
|
end,
|
|
}
|