packages feed

basen-bytestring (empty) → 0.1.0.0

raw patch · 11 files changed

+614/−0 lines, 11 filesdep +QuickCheckdep +basedep +basen-bytestringsetup-changed

Dependencies added: QuickCheck, base, basen-bytestring, bytestring

Files

+ ChangeLog.md view
@@ -0,0 +1,3 @@+# Changelog for basenstrings++## Unreleased changes
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Author William Fisher (c) 2018++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Author name here nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,31 @@+# basen-bytestring++[RFC-4648](https://tools.ietf.org/html/rfc4648) compliant base-n ByteStrings for+the common cases:++```+Data.ByteString.Base16+Data.ByteString.Base32+Data.ByteString.Base64+```++All with the same API:++```+module Data.ByteString.BaseN where++import qualified Data.ByteString.Char8 as B8++encode         :: B8.ByteString -> B8.ByteString+decode         :: B8.ByteString -> B8.ByteString++encodeAlphabet :: Enc -> B8.ByteString -> B8.ByteString+decodeAlphabet :: Enc -> B8.ByteString -> B8.ByteString+```++Currently `Data.ByteString.Base58` is not implemented although it is on the+roadmap.++The core `Data.ByteString.BaseN` contains all the common utilities for packing+bytes, processing ByteStrings, creating encodings, and encoding/decoding single+bytes.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ basen-bytestring.cabal view
@@ -0,0 +1,48 @@+name:           basen-bytestring+version:        0.1.0.0+description:    Base-N ByteStrings for base{16,32,58,64}+homepage:       https://github.com/FilWisher/basen-bytestring#readme+bug-reports:    https://github.com/FilWisher/basen-bytestring/issues+author:         William Fisher+maintainer:     williamsykesfisher@gmail.com+copyright:      2018 William Fisher+license:        BSD3+license-file:   LICENSE+build-type:     Simple+cabal-version:  >= 1.10++extra-source-files:+    ChangeLog.md+    README.md++source-repository head+  type: git+  location: https://github.com/FilWisher/basen-bytestring++library+  hs-source-dirs:+      src+  build-depends:+      base >=4.7 && <5+    , bytestring+  exposed-modules:+      Data.ByteString.Base16+      Data.ByteString.Base32+      Data.ByteString.Base58+      Data.ByteString.Base64+  other-modules:+      Data.ByteString.BaseN+  default-language: Haskell2010++test-suite basen-bytestring-test+  type: exitcode-stdio-1.0+  main-is: Properties.hs+  hs-source-dirs:+      test+  ghc-options: -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      base >=4.7 && <5+    , basen-bytestring+    , bytestring+    , QuickCheck+  default-language: Haskell2010
+ src/Data/ByteString/Base16.hs view
@@ -0,0 +1,76 @@+{-# LANGUAGE OverloadedStrings #-}++module Data.ByteString.Base16+  ( encode+  , decode+  ) where++import qualified Data.ByteString       as BS+import qualified Data.ByteString.Char8 as B8+import qualified Data.ByteString.Internal as Internal++import Foreign.Storable (peek, poke)+import Foreign.Ptr (plusPtr, Ptr)+import System.IO.Unsafe (unsafePerformIO)++import Data.ByteString.BaseN+import Data.Bits (shiftL, shiftR, (.&.), (.|.))+import Data.Word++encode :: B8.ByteString -> B8.ByteString+encode = encodeAlphabet hexlower++decode :: B8.ByteString -> Either String B8.ByteString+decode = decodeAlphabet hexlower++encodeAlphabet :: Enc -> B8.ByteString -> B8.ByteString+encodeAlphabet enc src@(Internal.PS sfp soff slen) =+  unsafePerformIO $ byChunk 1 (slen * 2) onchunk onend src+  where+    encodeNibble :: (Word8 -> Word8) -> Ptr Word8 -> IO Word8+    encodeNibble fn p = encodeWord enc . fn <$> peek p++    onchunk :: Ptr Word8 -> Ptr Word8 -> IO Int+    onchunk sp dp = do+      poke dp               =<< encodeNibble leftNibble  sp+      poke (dp `plusPtr` 1) =<< encodeNibble rightNibble sp+      return 2+    +    onend :: Ptr Word8 -> Ptr Word8 -> Int -> IO ()+    onend sp dp rem = return ()++decodeAlphabet :: Enc -> B8.ByteString -> Either String B8.ByteString+decodeAlphabet enc src@(Internal.PS sfp soff slen)+  | slen == 0 = Right BS.empty+  | otherwise = +    unsafePerformIO $ byChunkErr 2 dlen onchunk onend src+  where+    dlen = slen `div` 2++    onchunk :: Ptr Word8 -> Ptr Word8 -> IO (Either String Int)+    onchunk sp dp = do+      leftNibble  <- decodeWord enc <$> peek sp+      rightNibble <- decodeWord enc <$> peek (sp `plusPtr` 1)+      case (.|.) <$> fmap (`shiftL` 4) leftNibble <*> rightNibble of+        Left err -> do+          l <- peek sp :: IO Word8+          r <- peek (sp `plusPtr` 1) :: IO Word8+          return $ Left $ "Invalid byte encountered. One of: " ++ B8.unpack (BS.pack [l,r])+        Right w -> do+          poke dp w+          return $ Right 1++    onend :: Ptr Word8 -> Ptr Word8  -> Int -> IO (Either String Int)+    onend sp dp rem = onchunk sp dp >> return (Right dlen)++leftNibble :: Word8 -> Word8+leftNibble n = n `shiftR` 4++rightNibble :: Word8 -> Word8+rightNibble n = 0x0F .&. n++hexlower :: Enc+hexlower = mkEnc "0123456789abcdef"++hexupper :: Enc+hexupper = mkEnc "0123456789ABCDEF"
+ src/Data/ByteString/Base32.hs view
@@ -0,0 +1,140 @@+{-# LANGUAGE OverloadedStrings #-}++module Data.ByteString.Base32+  ( encode+  , decode++  , encodeHex+  , decodeHex+  ) where++import qualified Data.ByteString.Char8 as B8+import qualified Data.ByteString as BS+import qualified Data.ByteString.Internal as Internal+import Data.ByteString.BaseN++import Data.Word+import Data.Bits (shiftL, shiftR, (.|.), (.&.))++import Foreign.Ptr (plusPtr, Ptr)+import Foreign.Storable (peek, poke)+import System.IO.Unsafe (unsafePerformIO)++pack5 :: Int -> Word8 -> Word64 -> Word64+pack5 off word buf = buf .|. (fromIntegral word `shiftL` (off * 5))++pack8x5 :: [Word8] -> Word64+pack8x5 bs = foldr (uncurry pack8) 0 (zip [4,3..0] bs)++unpack8x5 :: Word64 -> [Word8]+unpack8x5 w =+  map (`unpack8` w) [4,3..0]++unpack5 :: Int -> Word64 -> Word8+unpack5 off buf = fromIntegral $ buf `shiftR` (5 * off) .&. 0x1f++pack5x8 :: [Word8] -> Word64+pack5x8 bs = foldr (uncurry pack5) 0 (zip [7,6..0] bs)++unpack5x8 :: Word64 -> [Word8]+unpack5x8 bs =+  map (`unpack5` bs) [7,6..0]++padCeilN :: Int -> Int -> Int+padCeilN n x+  | remd == 0 = x+  | otherwise = (x - remd) + n+  where  mask = n - 1+         remd = x .&. mask++encodeHex :: B8.ByteString -> B8.ByteString+encodeHex = encodeAlphabet alphabetHex++decodeHex :: B8.ByteString -> Either String B8.ByteString+decodeHex = decodeAlphabet alphabetHex++encode :: B8.ByteString -> B8.ByteString+encode = encodeAlphabet alphabet++decode :: B8.ByteString -> Either String B8.ByteString+decode = decodeAlphabet alphabet++encodeAlphabet :: Enc -> B8.ByteString -> B8.ByteString+encodeAlphabet enc src@(Internal.PS sfp soff slen) =+  unsafePerformIO $ byChunk 5 dlen onchunk onend src+  where+    (d, m) = (slen * 8) `divMod` 5+    dlen   = padCeilN 8 (d + if m == 0 then 0 else 1)++    onchunk sp dp = do+      words <- unpack5x8 . pack8x5 <$> traverse peek (map (sp `plusPtr`) [0..4])+      pokeN dp 8 $ map (encodeWord enc) words+      return 8++    onend sp dp 0 = return ()+    onend sp dp rem = do+      words <- unpack5x8 . pack8x5 <$> sequence+        [ peek sp +        , if rem > 1 then peek $ sp `plusPtr` 1 else return 0+        , if rem > 2 then peek $ sp `plusPtr` 2 else return 0+        , if rem > 3 then peek $ sp `plusPtr` 3 else return 0+        , if rem > 4 then peek $ sp `plusPtr` 4 else return 0+        ]+      let encoded = map (encodeWord enc) words+      poke dp               (encoded !! 0)+      poke (dp `plusPtr` 1) (encoded !! 1)+      poke (dp `plusPtr` 2) (if rem < 2 then 0x3d else encoded !! 2)+      poke (dp `plusPtr` 3) (if rem < 2 then 0x3d else encoded !! 3)+      poke (dp `plusPtr` 4) (if rem < 3 then 0x3d else encoded !! 4)+      poke (dp `plusPtr` 5) (if rem < 4 then 0x3d else encoded !! 5)+      poke (dp `plusPtr` 6) (if rem < 4 then 0x3d else encoded !! 6)+      poke (dp `plusPtr` 7) (0x3d :: Word8)++alphabet :: Enc+alphabet =  mkEnc "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"++alphabetHex :: Enc+alphabetHex =  mkEnc "0123456789ABCDEFGHIJKLMNOPQRSTUV"++decodeAlphabet :: Enc -> B8.ByteString -> Either String B8.ByteString+decodeAlphabet enc src@(Internal.PS sfp soff slen)+  | slen `mod` 8 /= 0 = Left "ByteString wrong length for valid Base32 encoding"+  | slen == 0         = Right BS.empty+  | otherwise         =+      unsafePerformIO $ byChunkErr 8 dlen onchunk onend src+      where+        (d, m) = (slen * 5) `divMod` 8+        dlen   = d + if m == 0 then 0 else 1++        -- Maps number of padding bytes in source buffer to number of valid bytes+        -- to write to destination buffer.+        validBytes :: Int -> Either String Int+        validBytes 0 = Right 5+        validBytes 1 = Right 4+        validBytes 3 = Right 3+        validBytes 4 = Right 2+        validBytes 6 = Right 1+        validBytes n = Left $ "Invalid amount of padding: " ++ show n++        decodeBytes = mapM $ decodeWord enc++        onchunk :: Ptr Word8 -> Ptr Word8 -> IO (Either String Int)+        onchunk sp dp = do+          words <- traverse (peek . (sp `plusPtr`)) [0..7]+          case (unpack8x5 . pack5x8) <$> decodeBytes words of+            Left err -> return $ Left err+            Right decoded -> do+              pokeN dp 5 decoded+              return $ Right 5++        onend :: Ptr Word8 -> Ptr Word8  -> Int -> IO (Either String Int)+        onend sp dp rem = do+          words <- traverse (peek . (sp `plusPtr`)) [0..7]+          let+            npad = length $ takeWhile (==0x3d) $ reverse words+            decode = (unpack8x5 . pack5x8) <$> decodeBytes (take (8-npad) words)+          case (,) <$> validBytes npad <*> decode of+            Left err -> return $ Left err+            Right (n, decoded) -> do+              pokeN dp n decoded+              return $ Right $ dlen - (5 - n)
+ src/Data/ByteString/Base58.hs view
@@ -0,0 +1,1 @@+module Data.ByteString.Base58 where
+ src/Data/ByteString/Base64.hs view
@@ -0,0 +1,130 @@+{-# LANGUAGE OverloadedStrings #-}++module Data.ByteString.Base64+  ( encode+  , decode+  +  , encodeURL+  , decodeURL+  ) where++import Data.Word+import Data.Bits (shiftL, shiftR, (.&.), (.|.))++import qualified Data.ByteString.Internal as Internal+import qualified Data.ByteString.Char8 as B8+import Data.ByteString.BaseN++import Foreign.Ptr (plusPtr, Ptr)+import Foreign.ForeignPtr (withForeignPtr)+import Foreign.Storable (peek, poke)+import System.IO.Unsafe (unsafePerformIO)++-- Pack a Word32 with a 6-bits at offset `off`. Offset is counted from the right+-- so `pack6 word 0 byte` packs the right-most 6 bits.+pack6 :: Int -> Word8 -> Word32 -> Word32+pack6 off word buf = buf .|. (fromIntegral word `shiftL` (off * 6))++-- Unpack 6 bits from a Word32 at offset `off`. Offset is counted form the right+-- so `unpack6 word 0` unpacks the 6 right-most bits into a Word8.+unpack6 :: Int -> Word32 -> Word8+unpack6 off buf = fromIntegral $ buf `shiftR` (6 * off) .&. 0x3f++pack8x3 :: [Word8] -> Word32+pack8x3 bs = foldr (uncurry pack8) 0 (zip [2,1,0] bs)++unpack8x3 :: Word32 -> [Word8]+unpack8x3 w =+  map (`unpack8` w) [2,1,0]++pack6x4 :: [Word8] -> Word32+pack6x4 bs = foldr (uncurry pack6) 0 (zip [3,2..0] bs)++unpack6x4 :: Word32 -> [Word8]+unpack6x4 bs =+  map (`unpack6` bs) [3,2..0]++encode :: B8.ByteString -> B8.ByteString+encode = encodeAlphabet alphabet++decode :: B8.ByteString -> Either String B8.ByteString+decode = decodeAlphabet alphabet++encodeURL :: B8.ByteString -> B8.ByteString+encodeURL = encodeAlphabet alphabetURL++decodeURL :: B8.ByteString -> Either String B8.ByteString+decodeURL = decodeAlphabet alphabetURL++decodeAlphabet :: Enc -> B8.ByteString -> Either String B8.ByteString+decodeAlphabet enc src@(Internal.PS sfp soff slen)+  | drem /= 0 = Left "ByteString wrong length for valid Base64 encoding padding"+  | dlen <= 0 = Right B8.empty+  | otherwise = +    unsafePerformIO $ byChunkErr 4 dlen onchunk onend src+    where+      (di, drem) = slen `divMod` 4+      dlen = di * 3++      onchunk :: Ptr Word8 -> Ptr Word8 -> IO (Either String Int)+      onchunk sp dp = do+        words <- traverse (peek . (sp `plusPtr`)) [0..3]+        case (unpack8x3 . pack6x4) <$> traverse (decodeWord enc) words of+          Left err -> return $ Left err+          Right decoded -> do+            pokeN dp 3 decoded+            return $ Right 3++      -- Maps number of padding bytes in source buffer to number of valid bytes+      -- to write to destination buffer.+      validBytes :: Int -> Either String Int+      validBytes 0 = Right 3+      validBytes 1 = Right 2+      validBytes 2 = Right 1+      validBytes n = Left $ "Invalid amount of padding: " ++ show n++      decodeBytes = mapM $ decodeWord enc++      onend :: Ptr Word8 -> Ptr Word8 -> Int -> IO (Either String Int)+      onend sp dp rem = do+        words <- traverse (peek . (sp `plusPtr`)) [0..3]+        let +          npad = length $ takeWhile (==0x3d) $ reverse words+          decode = (unpack8x3 . pack6x4) <$> decodeBytes (take (4-npad) words)+        case (,) <$> validBytes npad <*> decode of+          Left err -> return $ Left err+          Right (n, decoded) -> do+            pokeN dp n decoded+            return $ Right $ dlen - (3 - n)+++alphabet :: Enc+alphabet = mkEnc "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"++alphabetURL :: Enc+alphabetURL = mkEnc "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"++encodeAlphabet :: Enc -> B8.ByteString -> B8.ByteString+encodeAlphabet enc src@(Internal.PS sfp soff slen) =+  unsafePerformIO $ byChunk 3 dlen onchunk onend src++  where+    dlen = ((slen + 2) `div` 3) * 4++    onchunk sp dp = do+      w <- unpack6x4 . pack8x3 <$> traverse (peek . (sp `plusPtr`)) [0..2]+      pokeN dp 4 $ map (encodeWord enc) w+      return 4++    onend sp dp 0 = return ()+    onend sp dp rem = do+      w <- (unpack6x4 . pack8x3) <$> traverse (peek . (sp `plusPtr`)) [0..rem-1]+      let encoded = map (encodeWord enc) w+      poke dp               (encoded !! 0)+      poke (dp `plusPtr` 1) (encoded !! 1)+      poke (dp `plusPtr` 2) (if rem == 1 then 0x3d else encoded !! 2)+      poke (dp `plusPtr` 3) (0x3d :: Word8)++++
+ src/Data/ByteString/BaseN.hs view
@@ -0,0 +1,118 @@+{-# LANGUAGE OverloadedStrings #-}++module Data.ByteString.BaseN+  ( Enc(..)+  , mkEnc+  , encodeWord+  , decodeWord+  , pack8+  , unpack8+  , byChunk+  , byChunkErr+  , pokeN+  ) where++import qualified Data.ByteString.Internal as Internal+import qualified Data.ByteString.Char8 as B8+import qualified Data.ByteString as BS++import Data.Word+import Data.Bits (shiftL, shiftR, (.|.), (.&.), Bits)+import Data.Maybe (fromMaybe)++import Foreign.Ptr (plusPtr, minusPtr, Ptr)+import Foreign.Storable (poke)+import Foreign.ForeignPtr (withForeignPtr)++pokeN :: Ptr Word8 -> Int -> [Word8] -> IO ()+pokeN dp n bs =+  mapM_ insert $ zip [0..n-1] bs+  where+    insert (off, v) = poke (dp `plusPtr` off) v++-- Pack a Word32 with a Word8 at offset `off`. Offset is counted from the right+-- so `pack8 word 0 byte` packs the word with the right-most byte:+pack8 :: (Num a, Bits a) => Int -> Word8 -> a -> a+pack8 off word buf = buf .|. (fromIntegral word `shiftL` (off * 8))++-- Unpack a byte from a Word32 at offset `off`. Offset is counted form the right+-- so `unpack8 word 0` unpacks the right-most byte.+unpack8 :: (Integral a, Bits a) => Int -> a -> Word8+unpack8 off buf = fromIntegral $ buf `shiftR` (8 * off) .&. 0xff++-- Enc represents an encoding. The first ByteString is the map from decoded+-- bytes to encoded bytes.  The second ByteString is the map from encoded bytes+-- to decoded bytes.+--+-- The assumptions with Enc types is:+--  o That the encoded alphabet does not contain the byte 255 (it is used as a+--    sentinal)+--  o That the alphabet supplied to `mkEnc` is long enough for the encoding: no+--    bounds checking is done.+--+data Enc = Enc +  { encmap  :: B8.ByteString+  , decmap  :: B8.ByteString+  }+  deriving (Show)++-- Produce an `Enc` from an encoding alphabet. The decoding alphabet is derived+-- from the encoding alphabet. Non-present characters are signified with the+-- byte 0xFF. This means 0xFF cannot be present in the encoding alphabet.+mkEnc :: B8.ByteString -> Enc+mkEnc table = Enc table $+  BS.pack +    [ fromIntegral $ fromMaybe 0xFF (i `BS.elemIndex` table) +    | i <- [0..254] +    ]++encodeWord :: Enc -> Word8 -> Word8+encodeWord (Enc enc _) word = enc `BS.index` fromIntegral word++decodeWord :: Enc -> Word8 -> Either String Word8+decodeWord (Enc _ dec) word = case dec `BS.index` fromIntegral word of+  255 -> Left ("Not valid encoding: " ++ show word)+  n   -> Right n++byChunk :: +  Int -> +  Int ->+  (Ptr Word8 -> Ptr Word8 -> IO Int) -> +  (Ptr Word8 -> Ptr Word8 -> Int -> IO ()) -> +  B8.ByteString -> +  IO B8.ByteString+byChunk chunksize dlen onchunk onend (Internal.PS sfp soff slen) =+  withForeignPtr sfp $ \sptr -> do+    let+      end = sptr `plusPtr` (soff + slen)+      loop sp dp+        | sp `plusPtr` (chunksize-1) >= end = onend sp dp (end `minusPtr` sp)+        | otherwise               = do+          advance <- onchunk sp dp+          loop (sp `plusPtr` chunksize) (dp `plusPtr` advance)+    dfp <- Internal.mallocByteString dlen+    withForeignPtr dfp $ \dptr -> do+      loop sptr dptr+      return $ Internal.PS dfp 0 dlen++byChunkErr :: +  Int ->+  Int ->+  (Ptr Word8 -> Ptr Word8 -> IO (Either String Int)) ->+  (Ptr Word8 -> Ptr Word8 -> Int -> IO (Either String Int)) ->+  B8.ByteString ->+  IO (Either String B8.ByteString)+byChunkErr chunksize dlen onchunk onend (Internal.PS sfp soff slen) =+  withForeignPtr sfp $ \sptr -> do+    let+      end = sptr `plusPtr` (soff + slen)+      loop sp dp+        | sp `plusPtr` chunksize > end  = return $ Left "Not enough bytes remaining"+        | sp `plusPtr` chunksize == end = onend sp dp (end `minusPtr` sp)+        | otherwise               = do+          advance <- onchunk sp dp+          case advance of+            Left err -> return $ Left err+            Right n -> loop (sp `plusPtr` chunksize) (dp `plusPtr` n)+    dfp <- Internal.mallocByteString dlen+    fmap (Internal.PS dfp 0) <$> withForeignPtr dfp (loop sptr)
+ test/Properties.hs view
@@ -0,0 +1,35 @@+{-# LANGUAGE TemplateHaskell #-}++import Test.QuickCheck (Arbitrary(..), choose, forAll)+import Test.QuickCheck.All (quickCheckAll)+import Control.Monad (void)+import Data.Word++import qualified Data.ByteString.Base64 as Base64+import qualified Data.ByteString.Base32 as Base32+import qualified Data.ByteString.Base16 as Base16+import qualified Data.ByteString.Char8  as B8++instance Arbitrary B8.ByteString where+  arbitrary = B8.pack <$> arbitrary++prop_base64_inverse :: B8.ByteString -> Bool+prop_base64_inverse bs =+  either (const False) (bs==)+    $ Base64.decode $ Base64.encode bs++prop_base16_inverse :: B8.ByteString -> Bool+prop_base16_inverse bs =+  either (const False) (bs==)+    $ Base16.decode $ Base16.encode bs++prop_base32_inverse :: B8.ByteString -> Bool+prop_base32_inverse bs =+  either (const False) (bs==)+    $ Base32.decode $ Base32.encode bs++return []+runTests = $quickCheckAll++main :: IO ()+main = void runTests