packages feed

hnix-store-core-0.3.0.0: src/System/Nix/Internal/Base32.hs

module System.Nix.Internal.Base32 where

import Data.Bits (shiftR)
import Data.Char (chr, ord)
import Data.Word (Word8)
import Data.List (unfoldr)
import Data.Maybe (isJust, catMaybes)
import qualified Data.ByteString        as BS
import qualified Data.ByteString.Char8  as BSC
import qualified Data.Text              as T
import qualified Data.Vector            as V
import Numeric (readInt)

-- omitted: E O U T
digits32 = V.fromList "0123456789abcdfghijklmnpqrsvwxyz"

-- | Encode a 'BS.ByteString' in Nix's base32 encoding
encode :: BS.ByteString -> T.Text
encode c = T.pack $ map char32 [nChar - 1, nChar - 2 .. 0]
  where
    -- Each base32 character gives us 5 bits of information, while
    -- each byte gives is 8. Because 'div' rounds down, we need to add
    -- one extra character to the result, and because of that extra 1
    -- we need to subtract one from the number of bits in the
    -- bytestring to cover for the case where the number of bits is
    -- already a factor of 5. Thus, the + 1 outside of the 'div' and
    -- the - 1 inside of it.
    nChar = fromIntegral $ ((BS.length c * 8 - 1) `div` 5) + 1

    byte = BS.index c . fromIntegral

    -- May need to switch to a more efficient calculation at some
    -- point.
    bAsInteger :: Integer
    bAsInteger = sum [fromIntegral (byte j) * (256 ^ j)
                     | j <- [0 .. BS.length c - 1]
                     ]

    char32 :: Integer -> Char
    char32 i = digits32 V.! digitInd
      where
        digitInd = fromIntegral $
                   bAsInteger
                   `div` (32^i)
                   `mod` 32

-- | Decode Nix's base32 encoded text
decode :: T.Text -> Either String BS.ByteString
decode what = case T.all (flip elem digits32) what of
  True  -> unsafeDecode what
  False -> Left "Invalid base32 string"

-- | Decode Nix's base32 encoded text
-- Doesn't check if all elements match `digits32`
unsafeDecode :: T.Text -> Either String BS.ByteString
unsafeDecode what =
  case readInt 32
         (flip elem digits32)
         (\c -> maybe (error "character not in digits32") id $
                  V.findIndex (==c) digits32)
         (T.unpack what)
    of
      [(i, _)] -> Right $ padded $ integerToBS i
      x        -> Left $ "Can't decode: readInt returned " ++ show x
  where
    padded x | BS.length x < decLen = x `BS.append`
      (BSC.pack $ take (decLen - BS.length x) (cycle "\NUL"))
    padded x | otherwise = x

    decLen = T.length what * 5 `div` 8

-- | Encode an Integer to a bytestring
-- Similar to Data.Base32String (integerToBS) without `reverse`
integerToBS :: Integer -> BS.ByteString
integerToBS 0 = BS.pack [0]
integerToBS i
    | i > 0     = BS.pack $ unfoldr f i
    | otherwise = error "integerToBS not defined for negative values"
  where
    f 0 = Nothing
    f x = Just (fromInteger x :: Word8, x `shiftR` 8)