module Data.Base32String ( Base32String , b32String , b32String' , fromBinary , toBinary , fromBytes , toBytes , toText , fromText ) where import Control.Applicative (pure, (<$>)) import Control.Monad (liftM) import Data.Bits (shiftL, shiftR, (.|.)) import Data.Char (chr, ord) import Data.List (unfoldr) import Data.Maybe (fromJust, fromMaybe, isJust, listToMaybe) import Data.String (fromString) import Data.Word (Word8) import Numeric (readInt, showIntAtBase) import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as BS8 import qualified Data.ByteString.Lazy as BSL import qualified Data.Text as T import qualified Data.Text.Encoding as TE import qualified Data.Binary as B (Binary, decode, encode) -- | Represents a Base32 string. Guarantees that all characters it contains -- are valid base32 characters. data Base32String = Base32String BS.ByteString deriving ( Show, Eq, Ord ) -- | Smart constructor which validates that all the text are actually -- base-32 characters. b32String :: BS.ByteString -- ^ Our Base32 mapping table -> BS.ByteString -- ^ Our Base32 string -> Base32String b32String table bs = if BS.all (isValidBase32 table) bs then Base32String bs else error ("Not a valid base32 string: " ++ show bs) -- | Case insensitive variant of 'b32String', which converts all characters -- to upper case. b32String' :: BS.ByteString -- ^ Our Base32 mapping table -> BS.ByteString -- ^ Our Base32 string -> Base32String b32String' table bs = b32String table (TE.encodeUtf8 . T.toUpper . TE.decodeUtf8 $ bs) -- | Converts a 'B.Binary' to a 'Base32String' value fromBinary :: B.Binary a => BS.ByteString -- ^ Our Base32 mapping table -> a -- ^ Input object that is convertable to binary -> Base32String -- ^ Base32 representation of binary data fromBinary table = b32String table . b32Encode table . BSL.toStrict . B.encode -- | Converts a 'Base32String' to a 'B.Binary' value toBinary :: B.Binary a => BS.ByteString -- ^ Base32 mapping table -> Base32String -- ^ Base32 representation -> a -- ^ Converted object toBinary table (Base32String bs) = B.decode . BSL.fromStrict . fromMaybe (error "not a valid base32 input") $ b32Decode table bs -- | Reads a 'BS.ByteString' as raw bytes and converts to base32 representation. We -- cannot use the instance Binary of 'BS.ByteString' because it provides -- a leading length, which is not what we want when dealing with raw bytes. fromBytes :: BS.ByteString -- ^ Our Base32 mapping table -> BS.ByteString -- ^ Raw binary bytes -> Base32String -- ^ Base32 representation of raw binary bytes fromBytes table = b32String table . b32Encode table -- | Access to the raw bytes in a 'BS.ByteString' format. toBytes :: BS.ByteString -- ^ Base32 mapping table -> Base32String -- ^ Base32 string we wish to get binary data from -> BS.ByteString -- ^ Raw binary representation toBytes table (Base32String bs) = fromMaybe (error "not a valid base32 input") $ b32Decode table bs -- | Access to a 'T.Text' representation of the 'Base32String' toText :: Base32String -> T.Text toText (Base32String bs) = TE.decodeUtf8 bs -- | Converts a 'T.Text' representation to a 'Base32String' fromText :: BS.ByteString -- ^ Base32 mapping table -> T.Text -- ^ Text representation -> Base32String -- ^ Base32 classified representation fromText table = b32String table . TE.encodeUtf8 isValidBase32 :: BS.ByteString -> Word8 -> Bool isValidBase32 table c = c `BS.elem` table b32 :: BS.ByteString -> Word8 -> Word8 b32 table i = BS.index table (fromIntegral i) b32' :: BS.ByteString -> Word8 -> Maybe Word8 b32' table w = fromIntegral <$> BS.elemIndex w table b32EncodeInt :: BS.ByteString -- ^ Base32 mapping table -> Integer -> BS.ByteString b32EncodeInt table i = fromString $ showIntAtBase (32 :: Integer) f (fromIntegral i) "" where f = chr . fromIntegral . b32 table . fromIntegral b32DecodeInt :: BS.ByteString -- ^ Base32 mapping table -> BS.ByteString -> Maybe Integer b32DecodeInt table s = case go of Just (r,[]) -> Just r _ -> Nothing where c = b32' table . fromIntegral . ord p = isJust . c f = fromIntegral . fromJust . c go = listToMaybe $ readInt 32 p f (BS8.unpack s) b32Encode :: BS.ByteString -- ^ Base32 mapping table -> BS.ByteString -> BS.ByteString b32Encode table input = BS.append l r where (z,b) = BS.span (== 0) input l = BS.map (b32 table) z -- preserve leading 0's r | BS.null b = BS.empty | otherwise = b32EncodeInt table $ bsToInteger b b32Decode :: BS.ByteString -- ^ Base32 mapping table -> BS.ByteString -> Maybe BS.ByteString b32Decode table input = liftM (BS.append prefix) r where (z,b) = BS.span (== b32 table 0) input prefix = BS.map (fromJust . b32' table) z -- preserve leading 1's r | BS.null b = Just BS.empty | otherwise = integerToBS <$> b32DecodeInt table b -- | Decode a big endian Integer from a bytestring bsToInteger :: BS.ByteString -> Integer bsToInteger = foldr f 0 . reverse . BS.unpack where f w n = toInteger w .|. shiftL n 8 -- | Encode an Integer to a bytestring as big endian integerToBS :: Integer -> BS.ByteString integerToBS 0 = BS.pack [0] integerToBS i | i > 0 = BS.pack $ reverse $ unfoldr f i | otherwise = error "integerToBS not defined for negative values" where f 0 = Nothing f x = Just (fromInteger x :: Word8, x `shiftR` 8)