diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,3 @@
+# Changelog for basenstrings
+
+## Unreleased changes
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -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.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/basen-bytestring.cabal b/basen-bytestring.cabal
new file mode 100644
--- /dev/null
+++ b/basen-bytestring.cabal
@@ -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
diff --git a/src/Data/ByteString/Base16.hs b/src/Data/ByteString/Base16.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/ByteString/Base16.hs
@@ -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"
diff --git a/src/Data/ByteString/Base32.hs b/src/Data/ByteString/Base32.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/ByteString/Base32.hs
@@ -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)
diff --git a/src/Data/ByteString/Base58.hs b/src/Data/ByteString/Base58.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/ByteString/Base58.hs
@@ -0,0 +1,1 @@
+module Data.ByteString.Base58 where
diff --git a/src/Data/ByteString/Base64.hs b/src/Data/ByteString/Base64.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/ByteString/Base64.hs
@@ -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)
+
+
+
+
diff --git a/src/Data/ByteString/BaseN.hs b/src/Data/ByteString/BaseN.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/ByteString/BaseN.hs
@@ -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)
diff --git a/test/Properties.hs b/test/Properties.hs
new file mode 100644
--- /dev/null
+++ b/test/Properties.hs
@@ -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
