packages feed

locators-0.3.0.5: lib/Data/Locator/Hashes.hs

{-# LANGUAGE OverloadedStrings #-}

module Data.Locator.Hashes (
    toBase62,
    fromBase62,
    padWithZeros,
    hashStringToBase62,
) where

import Prelude hiding (toInteger)

import Crypto.Hash as Crypto
import qualified Data.ByteArray as B
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as S
import Data.Char (chr, isDigit, isLower, isUpper, ord)
import Data.Word
import Numeric (showIntAtBase)

--
-- Conversion between decimal and base 62
--

represent :: Int -> Char
represent x
    | x < 10 = chr (48 + x)
    | x < 36 = chr (65 + x - 10)
    | x < 62 = chr (97 + x - 36)
    | otherwise = '@'

toBase62 :: Integer -> String
toBase62 x =
    showIntAtBase 62 represent x ""

--

{- | Utility function to prepend \'0\' characters to a string representing a
 number. This allows you to ensure a fixed width for numbers that are less
 than the desired width in size. This comes up frequently when representing
 numbers in other bases greater than 10 as they are inevitably presented as
 text, and not having them evenly justified can (at best) be ugly and (at
 worst) actually lead to parsing and conversion bugs.
-}
padWithZeros :: Int -> String -> String
padWithZeros digits str =
    pad ++ str
  where
    pad = take len (replicate digits '0')
    len = digits - length str

value :: Char -> Int
value c
    | isDigit c = ord c - 48
    | isUpper c = ord c - 65 + 10
    | isLower c = ord c - 97 + 36
    | otherwise = 0

multiply :: Integer -> Char -> Integer
multiply acc c =
    acc * 62 + (fromIntegral $ value c)

fromBase62 :: String -> Integer
fromBase62 ss =
    foldl multiply 0 ss

concatToInteger :: [Word8] -> Integer
concatToInteger bytes =
    foldl fn 0 bytes
  where
    fn acc b = (acc * 256) + (fromIntegral b)

digest :: String -> Integer
digest ws =
    i
  where
    i = concatToInteger h
    h = B.unpack h'
    h' = Crypto.hash x' :: Crypto.Digest Crypto.SHA1
    x' = S.pack ws

--

{- | Take an arbitrary string, hash it, then pad it with zeros up to be a
 @digits@-long string in base 62.

 You may be interested to know that the 160-bit SHA1 hash used here can be
 expressed without loss as 27 digits of base 62, for example:

 >>> hashStringToBase62 27 "Hello World"
 1T8Sj4C5jVU6iQXCwCwJEPSWX6u
-}
hashStringToBase62 :: Int -> ByteString -> ByteString
hashStringToBase62 digits s' =
    r'
  where
    s = S.unpack s'
    n = digest s -- SHA1 hash
    limit = 62 ^ digits
    x = mod n limit -- trim to specified number base62 chars
    str = toBase62 x
    r = padWithZeros digits str -- convert to String
    r' = S.pack r