Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 117 additions & 85 deletions lib/hashids.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,25 @@ defmodule Hashids do

"""

defstruct [
alphabet: [], salt: [], min_len: 0,
a_len: 0, seps: [], s_len: 0, guards: [], g_len: 0,
]
defstruct alphabet: [], salt: [], min_len: 0, a_len: 0, seps: [], s_len: 0, guards: [], g_len: 0

@typep t :: %Hashids{
alphabet: charlist, salt: charlist, min_len: non_neg_integer,
a_len: non_neg_integer, seps: charlist, s_len: non_neg_integer, guards: charlist,
g_len: non_neg_integer,
}
alphabet: charlist,
salt: charlist,
min_len: non_neg_integer,
a_len: non_neg_integer,
seps: charlist,
s_len: non_neg_integer,
guards: charlist,
g_len: non_neg_integer
}

@min_alphabet_len 16
@sep_div 3.5
@guard_div 12

@default_alphabet 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'
@seps 'cfhistuCFHISTU'
@default_alphabet ~c"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
@seps ~c"cfhistuCFHISTU"

alias Hashids.Helpers

Expand Down Expand Up @@ -57,17 +59,26 @@ defmodule Hashids do

alphabet = Helpers.consistent_shuffle(alphabet, salt)
guard_count = trunc(Float.ceil(a_len / @guard_div))
{alphabet, guards, seps, a_len} = if a_len < 3 do
{guards, seps} = Enum.split(seps, guard_count)
{alphabet, guards, seps, a_len}
else
{guards, alphabet} = Enum.split(alphabet, guard_count)
a_len = a_len - guard_count
{alphabet, guards, seps, a_len}
end

{alphabet, guards, seps, a_len} =
if a_len < 3 do
{guards, seps} = Enum.split(seps, guard_count)
{alphabet, guards, seps, a_len}
else
{guards, alphabet} = Enum.split(alphabet, guard_count)
a_len = a_len - guard_count
{alphabet, guards, seps, a_len}
end

%Hashids{
alphabet: alphabet, salt: salt, min_len: min_len,
a_len: a_len, seps: seps, s_len: length(seps), guards: guards, g_len: length(guards),
alphabet: alphabet,
salt: salt,
min_len: min_len,
a_len: a_len,
seps: seps,
s_len: length(seps),
guards: guards,
g_len: length(guards)
}
end

Expand All @@ -84,29 +95,39 @@ defmodule Hashids do
end

def encode(s, numbers) when is_list(numbers) do
{num_checksum, _} = Enum.reduce(numbers, {0, 100}, fn
num, _ when num < 0 or not is_integer(num) ->
raise Hashids.Error, message: "Expected a non-negative integer. Got: #{inspect num}"
num, {cksm, i} ->
{cksm + rem(num, i), i+1}
end)
{num_checksum, _} =
Enum.reduce(numbers, {0, 100}, fn
num, _ when num < 0 or not is_integer(num) ->
raise Hashids.Error, message: "Expected a non-negative integer. Got: #{inspect(num)}"

num, {cksm, i} ->
{cksm + rem(num, i), i + 1}
end)

%Hashids{
alphabet: alphabet, salt: salt, min_len: min_len,
a_len: a_len, seps: seps, s_len: s_len, guards: guards, g_len: g_len,
alphabet: alphabet,
salt: salt,
min_len: min_len,
a_len: a_len,
seps: seps,
s_len: s_len,
guards: guards,
g_len: g_len
} = s

lottery = Enum.at(alphabet, rem(num_checksum, a_len))

{precipher, p_len, alphabet} =
preencode(numbers, 0, [lottery], 1, [lottery|salt], alphabet, a_len, seps, s_len)
preencode(numbers, 0, [lottery], 1, [lottery | salt], alphabet, a_len, seps, s_len)

{interm_cipher, i_len} =
extend_precipher1(precipher, p_len, min_len, num_checksum, guards, g_len)

{interm_cipher, i_len} =
extend_precipher2(interm_cipher, i_len, min_len, num_checksum, guards, g_len)

extend_cipher(interm_cipher, i_len, min_len, alphabet, a_len)
|> List.to_string
|> List.to_string()
end

@doc """
Expand All @@ -115,26 +136,32 @@ defmodule Hashids do
@spec decode(t, iodata) :: {:ok, [non_neg_integer]} | {:error, :invalid_input_data}

def decode(
%Hashids{alphabet: alphabet, salt: salt, a_len: a_len, seps: seps, guards: guards},
data) when is_list(data) or is_binary(data)
do
%Hashids{alphabet: alphabet, salt: salt, a_len: a_len, seps: seps, guards: guards},
data
)
when is_list(data) or is_binary(data) do
try do
cipher_split_at_guards =
String.split(IO.iodata_to_binary(data), Enum.map(guards, &<<&1::utf8>>))
cipher_part = case cipher_split_at_guards do
[_, x] -> x
[_, x, _] -> x
[x|_] -> x
end

result = if cipher_part != "" do
{<<lottery::utf8>>, rest_part} = String.split_at(cipher_part, 1)
rkey = [lottery|salt]
String.split(rest_part, Enum.map(seps, &<<&1::utf8>>))
|> decode_parts(rkey, alphabet, a_len, [])
else
[]
end
cipher_part =
case cipher_split_at_guards do
[_, x] -> x
[_, x, _] -> x
[x | _] -> x
end

result =
if cipher_part != "" do
{<<lottery::utf8>>, rest_part} = String.split_at(cipher_part, 1)
rkey = [lottery | salt]

String.split(rest_part, Enum.map(seps, &<<&1::utf8>>))
|> decode_parts(rkey, alphabet, a_len, [])
else
[]
end

{:ok, result}
rescue
_error -> {:error, :invalid_input_data}
Expand Down Expand Up @@ -165,27 +192,33 @@ defmodule Hashids do
#

defp uniquify_chars(charlist) do
uniquify_chars(charlist, [], MapSet.new, 0)
uniquify_chars(charlist, [], MapSet.new(), 0)
end

defp uniquify_chars([], acc, set, nchars), do: {Enum.reverse(acc), set, nchars}

defp uniquify_chars([char|rest], acc, set, nchars) do
defp uniquify_chars([char | rest], acc, set, nchars) do
if MapSet.member?(set, char) do
uniquify_chars(rest, acc, set, nchars)
else
uniquify_chars(rest, [char|acc], MapSet.put(set, char), nchars+1)
uniquify_chars(rest, [char | acc], MapSet.put(set, char), nchars + 1)
end
end

defp parse_option!(:alphabet, kw) do
list = case Keyword.fetch(kw, :alphabet) do
:error -> @default_alphabet
{:ok, bin} when is_binary(bin) -> String.to_charlist(bin)
_ ->
message = "Alphabet has to be a string of at least 16 characters/codepoints."
raise Hashids.Error, message: message
end
list =
case Keyword.fetch(kw, :alphabet) do
:error ->
@default_alphabet

{:ok, bin} when is_binary(bin) ->
String.to_charlist(bin)

_ ->
message = "Alphabet has to be a string of at least 16 characters/codepoints."
raise Hashids.Error, message: message
end

{uniq_alphabet, set, nchars} = uniquify_chars(list)
:ok = validate_alphabet!(set, nchars)
{uniq_alphabet, nchars}
Expand Down Expand Up @@ -218,16 +251,19 @@ defmodule Hashids do
msg = "Spaces in the alphabet are not allowed."
raise Hashids.Error, message: msg

true -> :ok
true ->
:ok
end
end

defp calculate_seps(seps, alphabet, a_len, salt) do
{seps, alphabet, a_len} = filter_seps(seps, [], alphabet, a_len)
seps = Helpers.consistent_shuffle(seps, salt)
s_len = length(seps)

if s_len == 0 or a_len / s_len > @sep_div do
new_len = max(2, trunc(Float.ceil(a_len / @sep_div)))

if new_len > s_len do
diff = new_len - s_len
{left, right} = Enum.split(alphabet, diff)
Expand All @@ -248,28 +284,27 @@ defmodule Hashids do
{Enum.reverse(seps), alphabet, a_len}
end

defp filter_seps([char|rest], seps, alphabet, a_len) do
defp filter_seps([char | rest], seps, alphabet, a_len) do
if j = Enum.find_index(alphabet, &(&1 == char)) do
# alphabet should not contains seps
{left, [_|right]} = Enum.split(alphabet, j)
{left, [_ | right]} = Enum.split(alphabet, j)
new_alphabet = left ++ right
filter_seps(rest, [char|seps], new_alphabet, a_len-1)
filter_seps(rest, [char | seps], new_alphabet, a_len - 1)
else
# seps should contain only characters present in alphabet
filter_seps(rest, seps, alphabet, a_len)
end
end


defp preencode([num], _, inret, p_len, rkey, alphabet, a_len, _, _) do
{outret, step_len, new_alphabet, _} = preencode_step(num, inret, rkey, alphabet, a_len)
{outret, p_len+step_len, new_alphabet}
{outret, p_len + step_len, new_alphabet}
end

defp preencode([num|rest], i, inret, p_len, rkey, alphabet, a_len, seps, seps_len) do
defp preencode([num | rest], i, inret, p_len, rkey, alphabet, a_len, seps, seps_len) do
{outret, step_len, new_alphabet, last} = preencode_step(num, inret, rkey, alphabet, a_len)
ret = seps_step(last, i, num, outret, seps, seps_len)
preencode(rest, i+1, ret, p_len+step_len+1, rkey, new_alphabet, a_len, seps, seps_len)
preencode(rest, i + 1, ret, p_len + step_len + 1, rkey, new_alphabet, a_len, seps, seps_len)
end

defp preencode_step(num, ret, rkey, alphabet, a_len) do
Expand All @@ -279,35 +314,33 @@ defmodule Hashids do
{[ret | last], last_len, enc_alphabet, last}
end

defp seps_step([char|_], i, num, ret, seps, seps_len) do
index = rem(num, char+i) |> rem(seps_len)
defp seps_step([char | _], i, num, ret, seps, seps_len) do
index = rem(num, char + i) |> rem(seps_len)
[ret, Enum.at(seps, index)]
end


defp extend_precipher1(precipher, p_len, min_len, num_cksm, guards, g_len)
when p_len < min_len
do
when p_len < min_len do
char = nested_list_at(precipher, 0)
index = rem(num_cksm + char, g_len)
guard = Enum.at(guards, index)
{[guard|precipher], p_len+1}
{[guard | precipher], p_len + 1}
end

defp extend_precipher1(precipher, p_len, _, _, _, _), do: {precipher, p_len}

defp extend_precipher2(precipher, p_len, min_len, num_cksm, guards, g_len)
when p_len < min_len
do
when p_len < min_len do
char2 = nested_list_at(precipher, 2)
index = rem(num_cksm + char2, g_len)
guard = Enum.at(guards, index)
{[precipher, guard], p_len+1}
{[precipher, guard], p_len + 1}
end

defp extend_precipher2(precipher, p_len, _, _, _, _), do: {precipher, p_len}

defp extend_cipher(cipher, c_len, min_len, alphabet, a_len)
when c_len < min_len
do
when c_len < min_len do
new_alphabet = Helpers.consistent_shuffle(alphabet, alphabet)
half_len = div(a_len, 2)
{left, right} = Enum.split(new_alphabet, half_len)
Expand All @@ -316,26 +349,25 @@ defmodule Hashids do
new_c_len = c_len + a_len

excess = new_c_len - min_len

if excess > 0 do
new_cipher |> List.flatten |> Enum.drop(div(excess, 2)) |> Enum.take(min_len)
new_cipher |> List.flatten() |> Enum.drop(div(excess, 2)) |> Enum.take(min_len)
else
extend_cipher(new_cipher, new_c_len, min_len, new_alphabet, a_len)
end
end

defp extend_cipher(cipher, _, _, _, _), do: cipher


defp decode_parts([], _, _, _, acc), do: Enum.reverse(acc)

defp decode_parts([part|rest], rkey, alphabet, a_len, acc) do
defp decode_parts([part | rest], rkey, alphabet, a_len, acc) do
buffer = rkey ++ alphabet
dec_alphabet = Helpers.consistent_shuffle(alphabet, Enum.take(buffer, a_len))
number = Helpers.decode(String.to_charlist(part), dec_alphabet, a_len)
decode_parts(rest, rkey, dec_alphabet, a_len, [number|acc])
decode_parts(rest, rkey, dec_alphabet, a_len, [number | acc])
end


defp nested_list_at(list, i) when is_integer(i) and i >= 0 do
try do
do_nested_list_at(list, i)
Expand All @@ -345,17 +377,17 @@ defmodule Hashids do
end
end

defp do_nested_list_at([h|rest], i) when is_list(h) do
defp do_nested_list_at([h | rest], i) when is_list(h) do
new_i = do_nested_list_at(h, i)
do_nested_list_at(rest, new_i)
end

defp do_nested_list_at([h|_], 0) do
throw h
defp do_nested_list_at([h | _], 0) do
throw(h)
end

defp do_nested_list_at([_|rest], i) do
do_nested_list_at(rest, i-1)
defp do_nested_list_at([_ | rest], i) do
do_nested_list_at(rest, i - 1)
end

defp do_nested_list_at([], i) do
Expand Down
Loading