module Data.Hashids
( HashidsContext
, createHashidsContext
, hashidsSimple
, hashidsMinimum
, encodeHex
, decodeHex
, encode
, encodeList
, decode
, encodeUsingSalt
, encodeListUsingSalt
, decodeUsingSalt
, encodeHexUsingSalt
, decodeHexUsingSalt
) where
import Data.Char ( ord )
import Data.Foldable ( toList )
import Data.List ( (\\), elemIndex, foldl', unfoldr, nub, intersect )
import Data.List.Split ( split, oneOf, dropDelims, chunksOf )
import Data.Maybe ( fromMaybe )
import Data.Sequence ( Seq )
import Numeric ( showHex, readHex )
import qualified Data.Sequence as Seq
exchange :: Int -> Int -> Seq a -> Seq a
exchange i j seq = i <--> j $ j <--> i $ seq
where
a <--> b = Seq.update a $ Seq.index seq b
splitOn :: Eq a => [a] -> [a] -> [[a]]
splitOn delims = split (dropDelims $ oneOf delims)
(|>) :: a -> (a -> b) -> b
(|>) a f = f a
lookupMod :: Integral n => Int -> [a] -> n -> a
lookupMod modulo xs x = xs !! fromIntegral (x `mod` fromIntegral modulo)
data Alphabet = Alphabet
{ alphabetGlyphs :: String
, alphabetLength :: Int
, alphabetLookup :: Integral b => b -> Char
, alphabetIndex :: Char -> Int
}
mkAlphabet :: String -> Alphabet
mkAlphabet str = Alphabet str len (lookupMod len str) indexOf
where
indexOf ch = fromMaybe (error "Not in list!") (elemIndex ch str)
len = length str
data Salt = Salt
{ saltLookup :: Integral b => b -> Char
, saltLength :: Int }
mkSalt salt = Salt (lookupMod len salt) len
where
len = length salt
data HashidsContext = Context
{ guards :: !String
, seps :: !String
, salt :: !String
, minHashLength :: !Int
, alphabet :: !Alphabet
}
createHashidsContext :: String
-> Int
-> String
-> HashidsContext
createHashidsContext salt minHashLen alphabet
| length uniqueAlphabet < minAlphabetLength
= error $ "alphabet must contain at least " ++ show minAlphabetLength ++ " unique characters"
| ' ' `elem` uniqueAlphabet
= error "alphabet cannot contain spaces"
| null seps'' || fromIntegral (length alphabet') / fromIntegral (length seps'') > sepDiv
= case sepsLength length seps'' of
diff | diff > 0
-> res (drop diff alphabet') (seps'' ++ take diff alphabet')
_ -> res alphabet' (take sepsLength seps'')
| otherwise = res alphabet' seps''
where
res ab _seps =
let shuffled = consistentShuffleS ab salt
guardCount = ceiling (fromIntegral (length shuffled) / guardDiv)
context = Context
{ guards = take guardCount _seps
, seps = drop guardCount _seps
, salt = salt
, minHashLength = minHashLen
, alphabet = mkAlphabet shuffled }
in if length shuffled < 3
then context
else context{ guards = take guardCount shuffled
, seps = _seps
, alphabet = mkAlphabet $ drop guardCount shuffled }
seps' = uniqueAlphabet `intersect` seps
seps'' = consistentShuffleS seps' salt
sepsLength =
case ceiling (fromIntegral (length alphabet') / sepDiv) of
1 -> 2
n -> n
uniqueAlphabet = nub alphabet
alphabet' = uniqueAlphabet \\ seps
minAlphabetLength = 16
sepDiv = 3.5
guardDiv = 12
seps = "cfhistuCFHISTU"
defaultAlphabet :: String
defaultAlphabet = ['a'..'z'] ++ ['A'..'Z'] ++ "1234567890"
hashidsSimple :: String
-> HashidsContext
hashidsSimple salt = createHashidsContext salt 0 defaultAlphabet
hashidsMinimum :: String
-> Int
-> HashidsContext
hashidsMinimum salt minimum = createHashidsContext salt minimum defaultAlphabet
encodeHex :: HashidsContext
-> String
-> String
encodeHex context str
| not (all hexChar str) = ""
| otherwise = encodeList context $ map go $ chunksOf 12 str
where
go str = let [(a,_)] = readHex ('1':str) in a
hexChar c = c `elem` "0123456789abcdef"
decodeHex :: HashidsContext
-> String
-> String
decodeHex context hash =
concatMap (drop 1 . flip showHex "") numbers
where
numbers = decode context hash
numbersHashInt :: Integral a => [a] -> a
numbersHashInt xs = foldr ((+) . uncurry mod) 0 $ zip xs [100 .. ]
encode :: Integral n
=> HashidsContext
-> n
-> String
encode context n = encodeList context [n]
encodeList :: Integral n
=> HashidsContext
-> [n]
-> String
encodeList Context{ alphabet = alphabet@Alphabet{ alphabetLength = len }, .. } numbers =
res |> expand (++) 0
|> expand (flip (++)) 2
|> expand' alphabet'
where
(res, alphabet') = foldl' go ([lottery], alphabet) (zip [0 .. ] numbers)
expand coalesce index str = coalesce
[ lookupMod guardsLength guards $ ord (str !! index) + fromIntegral hashInt
| length str < minHashLength ] str
expand' ab str
| length str >= minHashLength = str
| otherwise =
let ab' = consistentShuffle_ ab len (alphabetGlyphs ab)
chars = alphabetGlyphs ab'
str' = concat [drop halfLength chars, str, take halfLength chars]
in expand' ab' $ case length str' minHashLength of
n | n > 0
-> take minHashLength $ drop (div n 2) str'
_ -> str'
hashInt = numbersHashInt numbers
lottery = alphabetLookup alphabet hashInt
prefix = lottery : salt
go (r, ab@Alphabet{..}) (i, number)
| number < 0 = error "all numbers must be non-negative"
| otherwise =
let ab' = consistentShuffle_ ab alphabetLength (prefix ++ alphabetGlyphs)
last = hash number ab'
n = (fromIntegral number `mod` (ord (head last) + i)) `mod` sepsLength
suffix = [seps !! n | i < lastNumber]
in (r ++ last ++ suffix, ab')
sepsLength = length seps
guardsLength = length guards
lastNumber = length numbers 1
halfLength = div len 2
decode :: Integral n
=> HashidsContext
-> String
-> [n]
decode _ "" = []
decode ctx@Context{..} hash
| "" == str = []
| encodeList ctx res /= hash = []
| otherwise = res
where
res = splitOn seps tail
|> foldl' go ([], alphabet)
|> fst
|> reverse
hashArray = splitOn guards hash
(Alphabet glyphs len _ _) = alphabet
str@(lottery:tail) =
hashArray !! case length hashArray of
0 -> error "Internal error."
2 -> 1
3 -> 1
_ -> 0
prefix = lottery : salt
go (xs, ab) ssh =
let buffer = prefix ++ alphabetGlyphs ab
ab' = consistentShuffle_ ab len buffer
in (unhash ssh ab':xs, ab')
consistentShuffleS :: String -> String -> String
consistentShuffleS alphabet salt = alphabetGlyphs $ consistentShuffle (mkAlphabet alphabet) (mkSalt salt)
consistentShuffle_ :: Alphabet -> Int -> String -> Alphabet
consistentShuffle_ alphabet len = consistentShuffle alphabet . mkSalt . take len
consistentShuffle :: Alphabet -> Salt -> Alphabet
consistentShuffle alphabet@Alphabet{..} Salt{..}
| 0 == saltLength = alphabet
| otherwise = mkAlphabet $ toList x
where
(_,x) = zip3 [len, pred len .. 1] xs ys |> foldl' go (0, Seq.fromList alphabetGlyphs)
xs = cycle [0 .. saltLength 1]
ys = map (ord . saltLookup) xs
go (p, ab) (i, v, ch) =
let shuffled = exchange i j ab
p' = p + ch
j = mod (ch + v + p') i
in (p', shuffled)
len = alphabetLength 1
unhash :: Integral n => String -> Alphabet -> n
unhash input Alphabet{..} = foldl' go 0 $ zip [len, len 1 .. ] input
where
go number (i, char) =
number + fromIntegral (alphabetIndex char * alphabetLength ^ i)
len = pred $ length input
hash :: Integral n => n -> Alphabet -> String
hash input Alphabet{..}
| 0 == input = [alphabetLookup input]
| otherwise = go (input, [])
where
len = fromIntegral alphabetLength
go (0, xs) = xs
go (x, xs) = go (div x len, alphabetLookup x:xs)
encodeUsingSalt :: Integral n
=> String
-> n
-> String
encodeUsingSalt = encode . hashidsSimple
encodeListUsingSalt :: Integral n
=> String
-> [n]
-> String
encodeListUsingSalt = encodeList . hashidsSimple
decodeUsingSalt :: Integral n
=> String
-> String
-> [n]
decodeUsingSalt = decode . hashidsSimple
encodeHexUsingSalt :: String
-> String
-> String
encodeHexUsingSalt = encodeHex . hashidsSimple
decodeHexUsingSalt :: String
-> String
-> String
decodeHexUsingSalt = decodeHex . hashidsSimple