diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,3 @@
+# Changelog for z85-bytestring
+
+## Unreleased changes
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Athan Clark (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 Athan Clark 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,12 @@
+# z85
+
+[z85](https://rfc.zeromq.org/spec:32/Z85/) is a binary string codec, like hexadecimal or base64, but has a higher density
+of compression than the former, due to its use of a higher base value of 85 than base 64. ByteStrings just need to be
+a length of a multiple of 4 (a Word32String might be a better name).
+
+There are multiple layers of exposed implementation in this package
+
+- `Word32 <-> Vector 4 Z85Char` for low level work
+- Attoparsec `Parser ByteString <-> Parser Text` for slightly higher level parsing of strict data
+- Pipes `Pipe ByteString Text <-> Pipe Text ByteString` for encoding / decoding streams of strict data
+- Casual `Lazy.ByteString ~ Lazy.Text` functions for encoding / decoding lazy data.
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/src/Data/Attoparsec/ByteString/Z85.hs b/src/Data/Attoparsec/ByteString/Z85.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Attoparsec/ByteString/Z85.hs
@@ -0,0 +1,17 @@
+module Data.Attoparsec.ByteString.Z85 where
+
+import Data.ByteString.Z85.Internal (Z85Chunk, encodeWord, printZ85Chunk)
+import Data.Attoparsec.ByteString (Parser)
+import Data.Attoparsec.Binary (anyWord32be)
+import Data.Text (Text)
+import Data.Foldable (fold)
+import Control.Applicative (many)
+
+
+
+anyWord32beEncoded :: Parser Z85Chunk
+anyWord32beEncoded = encodeWord <$> anyWord32be
+
+
+z85Encoded :: Parser Text
+z85Encoded = fold <$> many (printZ85Chunk <$> anyWord32beEncoded)
diff --git a/src/Data/Attoparsec/Text/Z85.hs b/src/Data/Attoparsec/Text/Z85.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Attoparsec/Text/Z85.hs
@@ -0,0 +1,40 @@
+module Data.Attoparsec.Text.Z85 where
+
+import Data.ByteString.Z85.Internal (Z85Char (..), Z85Chunk, decodeWord, isZ85Char)
+import qualified Data.Vector.Sized as V
+import Data.Maybe (fromJust)
+import Data.Word (Word32)
+import Data.ByteString (ByteString, empty)
+import Data.ByteString.Lazy (toStrict)
+import Data.ByteString.Builder (word32BE, toLazyByteString)
+import Data.Attoparsec.Text (Parser, char, satisfy, (<?>))
+import Control.Applicative (optional)
+import Control.Monad (replicateM)
+
+
+
+anyZ85Char :: Parser Z85Char
+anyZ85Char = Z85Char <$> satisfy isZ85Char <?> "Z85Char character class"
+
+
+z85Char :: Z85Char -> Parser Z85Char
+z85Char (Z85Char c) = Z85Char <$> char c
+
+
+anyZ85Chunk :: Parser Z85Chunk
+anyZ85Chunk =
+  (fromJust . V.fromList) <$> replicateM 5 anyZ85Char
+
+
+anyZ85ChunkDecoded :: Parser Word32
+anyZ85ChunkDecoded = decodeWord <$> anyZ85Chunk
+
+
+z85Decoded :: Parser ByteString
+z85Decoded = do
+  mX <- optional anyZ85ChunkDecoded
+  case mX of
+    Nothing -> pure empty
+    Just x ->
+      let x' = toStrict (toLazyByteString (word32BE x))
+      in  (x' <>) <$> z85Decoded
diff --git a/src/Data/ByteString/Z85.hs b/src/Data/ByteString/Z85.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/ByteString/Z85.hs
@@ -0,0 +1,108 @@
+{-# LANGUAGE
+    OverloadedStrings
+  #-}
+
+module Data.ByteString.Z85 where
+
+import qualified Pipes.Z85 as PZ
+import qualified Pipes.ByteString as PB
+import qualified Pipes.Text as PT
+import Pipes ((>->), Producer, Effect, for, runEffect)
+
+import qualified Data.ByteString as BS
+import Data.ByteString.Lazy (ByteString)
+import qualified Data.ByteString.Lazy as LBS
+import qualified Data.Text as T
+import Data.Text.Lazy (Text)
+import qualified Data.Text.Lazy as LT
+import Data.IORef (readIORef, newIORef, modifyIORef)
+import Control.Monad.IO.Class (liftIO)
+import Control.Exception (try, SomeException)
+import System.IO.Unsafe (unsafePerformIO)
+
+
+
+data Z85Error
+  = TextNotMod5
+  | BSNotMod4
+  | ParsingError PZ.Z85ParsingError
+  deriving (Eq, Show)
+
+
+-- | Fails by checking the length is @mod 4@ - warning, checking length on a potentially infinite stream may not be possible, or may
+-- cause faulty memory management. Use 'encode\'' if you want to verify offsets after.
+encode :: ByteString -> Either Z85Error Text
+encode bs
+  | LBS.length bs `mod` 4 /= 0 = Left BSNotMod4
+  | otherwise = unsafePerformIO $ do
+    let go :: IO Text
+        go = do
+          leftoverRef <- newIORef ""
+          let encoded :: Producer T.Text IO ()
+              encoded = PB.fromLazy bs >-> PZ.encode leftoverRef
+          PT.toLazyM encoded
+    eX <- try go
+    pure $ case eX of
+      Left e -> Left (ParsingError e)
+      Right x -> Right x
+
+-- | Fails by checking the residual unparsed input after doing all the work -
+-- use 'encode' if you want to check length early.
+encode' :: ByteString -> Either Z85Error Text
+encode' bs = unsafePerformIO $ do
+  let go :: IO (Either Z85Error Text)
+      go = do
+        leftoverRef <- newIORef ""
+        let encoded :: Producer T.Text IO ()
+            encoded = PB.fromLazy bs >-> PZ.encode leftoverRef
+        x <- PT.toLazyM encoded
+        leftover <- readIORef leftoverRef
+        pure $
+          if BS.length leftover /= 0
+          then Left BSNotMod4
+          else Right x
+  eX <- try go
+  pure $ case eX of
+    Left e -> Left (ParsingError e)
+    Right x -> x
+
+
+
+-- | Fails by checking the length is @mod 5@ - warning, checking length on a potentially infinite stream may not be possible, or may
+-- cause faulty memory management. Use 'decode\'' if you want to verify offsets after.
+decode :: Text -> Either Z85Error ByteString
+decode t
+  | LT.length t `mod` 5 /= 0 = Left TextNotMod5
+  | otherwise = unsafePerformIO $ do
+    let go :: IO ByteString
+        go = do
+          leftoverRef <- newIORef ""
+          let decoded :: Producer BS.ByteString IO ()
+              decoded = PT.fromLazy t >-> PZ.decode leftoverRef
+          PB.toLazyM decoded
+    eX <- try go
+    pure $ case eX of
+      Left e -> Left (ParsingError e)
+      Right x -> Right x
+
+-- | Fails by checking the residual unparsed input after doing all the work -
+-- use 'decode' if you want to check length early.
+decode' :: Text -> Either Z85Error ByteString
+decode' t = unsafePerformIO $ do
+  let go :: IO (Either Z85Error ByteString)
+      go = do
+        leftoverRef <- newIORef ""
+        let decoded :: Producer BS.ByteString IO ()
+            decoded = PT.fromLazy t >-> PZ.decode leftoverRef
+        x <- PB.toLazyM decoded
+        leftover <- readIORef leftoverRef
+        pure $
+          if T.length leftover /= 0
+          then Left TextNotMod5
+          else Right x
+  eX <- try go
+  pure $ case eX of
+    Left e -> Left (ParsingError e)
+    Right x -> x
+
+
diff --git a/src/Data/ByteString/Z85/Internal.hs b/src/Data/ByteString/Z85/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/ByteString/Z85/Internal.hs
@@ -0,0 +1,130 @@
+{-# LANGUAGE
+    DataKinds
+  , DeriveGeneric
+  #-}
+
+module Data.ByteString.Z85.Internal where
+
+import Data.Vector.Sized (Vector)
+import qualified Data.Vector.Sized as V
+import Data.Char (ord, isAlphaNum)
+import Data.Text (Text, pack)
+import Data.Maybe (fromJust)
+import Data.Bits ((.&.), shiftR)
+import Data.Word (Word32)
+import Data.STRef (newSTRef, modifySTRef, readSTRef)
+import Data.Traversable (traverse)
+import Control.Monad (forM_)
+import Control.Monad.ST (runST)
+import Test.QuickCheck (Arbitrary (..))
+import Test.QuickCheck.Gen (oneof)
+import GHC.Generics (Generic)
+
+
+
+type Base85 = Word32
+
+
+newtype Z85Char = Z85Char
+  { getZ85Char :: Char
+  } deriving (Eq, Ord, Show, Generic)
+
+instance Arbitrary Z85Char where
+  arbitrary = oneof $ map pure $ V.toList z85Chars
+
+
+type Z85Chunk = Vector 5 Z85Char
+
+
+printZ85Chunk :: Z85Chunk -> Text
+printZ85Chunk = pack . map getZ85Char . V.toList
+
+
+
+z85Chars :: Vector 85 Z85Char
+z85Chars = Z85Char <$> fromJust (V.fromList "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#")
+
+
+isZ85Char :: Char -> Bool
+isZ85Char c = isAlphaNum c || c `elem` ".-:+=^!/*?&<>()[]{}@%$#"
+
+
+
+lookupZ85Char :: Base85 -> Z85Char
+lookupZ85Char idx = z85Chars `V.unsafeIndex` fromIntegral idx
+
+
+charCodeToBase85 :: Vector 96 Base85
+charCodeToBase85 = fromJust (V.fromList
+  [ nan, 0x44, nan, 0x54, 0x53, 0x52, 0x48, nan
+  , 0x4B, 0x4C, 0x46, 0x41, nan, 0x3F, 0x3E, 0x45
+  , 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07
+  , 0x08, 0x09, 0x40, nan, 0x49, 0x42, 0x4A, 0x47
+  , 0x51, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A
+  , 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32
+  , 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A
+  , 0x3B, 0x3C, 0x3D, 0x4D, nan, 0x4E, 0x43, nan
+  , nan, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10
+  , 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18
+  , 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20
+  , 0x21, 0x22, 0x23, 0x4F, nan, 0x50, 0x00, 0x00
+  ])
+  where
+    nan = 0
+
+
+z85CharToIndex :: Z85Char -> Int
+z85CharToIndex (Z85Char c) = ord c - 32
+
+
+lookupBase85 :: Z85Char -> Base85
+lookupBase85 c = V.unsafeIndex charCodeToBase85 (z85CharToIndex c)
+
+
+encodeWord :: Word32 -> Z85Chunk
+encodeWord word =
+  let value = runST $ do
+        valueRef <- newSTRef 0
+        forM_ [0..3] $ \j ->
+          let base256 :: Word32
+              base256 = 0xFF
+              byteShift :: Int
+              byteShift = 8 * (4 - j - 1)
+              byteChunk :: Word32
+              byteChunk = shiftR word byteShift .&. base256
+          in  modifySTRef valueRef (\val -> (val * 256) + byteChunk)
+        readSTRef valueRef
+      getC :: Word32 -> Z85Char
+      getC n =
+        let divisor :: Word32
+            divisor = 85 ^ n
+            idx :: Base85
+            idx = (value `div` divisor) `mod` 85
+        in  lookupZ85Char idx
+  in  V.fromTuple (getC 4, getC 3, getC 2, getC 1, getC 0)
+
+
+
+decodeWord :: Z85Chunk -> Word32
+decodeWord chunk =
+  let value :: Word32
+      value = runST $ do
+        valueRef <- newSTRef 0
+        let addPartValue part =
+              modifySTRef valueRef (\val -> (val * 85) + part)
+        _ <- traverse addPartValue base85Values
+        readSTRef valueRef
+      divisor :: Int -> Word32
+      divisor n = 256 ^ n
+  in  runST $ do
+        wordRef <- newSTRef 0
+        forM_ [3,2..0] $ \n ->
+          let go word =
+                let magnitude = word * 256
+                    dust = (value `div` divisor n) `mod` 256
+                in  magnitude + dust
+          in  modifySTRef wordRef go
+        readSTRef wordRef
+  where
+    base85Values :: Vector 5 Base85
+    base85Values = lookupBase85 <$> chunk
diff --git a/src/Pipes/Z85.hs b/src/Pipes/Z85.hs
new file mode 100644
--- /dev/null
+++ b/src/Pipes/Z85.hs
@@ -0,0 +1,81 @@
+{-# LANGUAGE
+    OverloadedLists
+  , DeriveGeneric
+  #-}
+
+module Pipes.Z85 where
+
+import Data.Attoparsec.ByteString.Z85 (z85Encoded)
+import Data.Attoparsec.Text.Z85 (z85Decoded)
+
+import Pipes (Pipe, await, yield)
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as BS
+import Data.Text (Text)
+import qualified Data.Text as T
+import qualified Data.Attoparsec.ByteString as AB
+import qualified Data.Attoparsec.Text as AT
+import Data.IORef (IORef, readIORef, writeIORef)
+import Control.Monad.IO.Class (MonadIO, liftIO)
+import Control.Exception (throw, Exception)
+import GHC.Generics (Generic)
+
+
+
+data Z85ParsingError
+  = EncodeError [String] String
+  | DecodeError [String] String
+  | CantCompletePartial
+  deriving (Eq, Show, Generic)
+instance Exception Z85ParsingError
+
+
+encode :: MonadIO m
+       => IORef ByteString -- ^ Unparsed input
+       -> Pipe ByteString Text m ()
+encode leftoverRef = do
+  prev <- liftIO (readIORef leftoverRef)
+  r <-  let f current = AB.parse z85Encoded (prev <> current)
+        in  f <$> await
+  let onFail i es e = liftIO $ do
+        writeIORef leftoverRef i
+        throw (EncodeError es e)
+      onDone i x = do
+        liftIO (writeIORef leftoverRef i)
+        yield x
+        encode leftoverRef
+  case r of
+    AB.Fail i es e -> onFail i es e
+    AB.Partial f ->
+      case f BS.empty of
+        AB.Fail i es e -> onFail i es e
+        AB.Done i x -> onDone i x
+        AB.Partial _ ->
+          throw CantCompletePartial
+    AB.Done i x -> onDone i x
+
+
+
+decode :: MonadIO m
+       => IORef Text -- ^ Unparsed input
+       -> Pipe Text ByteString m ()
+decode leftoverRef = do
+  prev <- liftIO (readIORef leftoverRef)
+  r <-  let f current = AT.parse z85Decoded (prev <> current)
+        in  f <$> await
+  let onFail i es e = liftIO $ do
+        writeIORef leftoverRef i
+        throw (DecodeError es e)
+      onDone i x = do
+        liftIO (writeIORef leftoverRef i)
+        yield x
+        decode leftoverRef
+  case r of
+    AT.Fail i es e -> onFail i es e
+    AT.Partial f ->
+      case f T.empty of
+        AT.Fail i es e -> onFail i es e
+        AT.Done i x -> onDone i x
+        AT.Partial _ ->
+          throw CantCompletePartial
+    AT.Done i x -> onDone i x
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,83 @@
+{-# LANGUAGE
+    OverloadedStrings
+  #-}
+
+import qualified Data.ByteString.Z85 as Z85
+import Data.ByteString.Z85.Internal (encodeWord, decodeWord, printZ85Chunk)
+import Data.Attoparsec.ByteString.Z85 (z85Encoded, anyWord32beEncoded)
+import Data.Attoparsec.Text.Z85 (z85Decoded, anyZ85ChunkDecoded)
+
+import Test.Tasty (testGroup, defaultMain)
+import Test.Tasty.QuickCheck (testProperty)
+import Test.Tasty.HUnit (testCase, (@=?))
+import Test.QuickCheck (Property, (===))
+import Test.QuickCheck.Instances ()
+
+import Data.Word (Word32)
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as BS
+import Data.ByteString.Lazy (toStrict)
+import qualified Data.ByteString.Lazy as LBS
+import Data.ByteString.Builder (word8, word32BE, toLazyByteString)
+import qualified Data.Attoparsec.ByteString as AB
+import qualified Data.Attoparsec.Text as AT
+
+
+
+
+main :: IO ()
+main = defaultMain $ testGroup "All Tests"
+  [ testGroup "Unit Tests"
+    [ testCase "\"HelloWorld\" == 0x86,0x4F,0xD2,0x6F,0xB5,0x59,0xF7,0x5B" $
+      let bs = toStrict $ toLazyByteString $ foldMap word8 [0x86,0x4F,0xD2,0x6F,0xB5,0x59,0xF7,0x5B]
+      in  AB.parseOnly z85Encoded bs @=? Right "HelloWorld"
+    ]
+  , testGroup "Property Tests"
+    [ testProperty "decode / encode word iso" decodeWordIso
+    , testProperty "decode / encode word iso over attoparsec" decodeWordIso'
+    , testProperty "decode / encode sentence iso over attoparsec" encodeSentenceIsoA
+    , testProperty "decode / encode sentence iso" encodeSentenceIso
+    , testProperty "decode / encode sentence iso, residual failure" encodeSentenceIso'
+    ]
+  ]
+
+
+
+
+decodeWordIso :: Word32 -> Property
+decodeWordIso w = w === decodeWord (encodeWord w)
+
+
+
+decodeWordIso' :: Word32 -> Property
+decodeWordIso' w =
+  let bs = toStrict (toLazyByteString (word32BE w))
+  in  fmap (AT.parseOnly anyZ85ChunkDecoded . printZ85Chunk) (AB.parseOnly anyWord32beEncoded bs) === Right (Right w)
+
+
+
+encodeSentenceIsoA :: ByteString -> Property
+encodeSentenceIsoA x' =
+  let xMod = BS.length x' `mod` 4
+      x = if xMod /= 0
+            then BS.drop xMod x'
+            else x'
+  in  fmap (AT.parseOnly z85Decoded) (AB.parseOnly z85Encoded x) === Right (Right x)
+
+
+encodeSentenceIso :: LBS.ByteString -> Property
+encodeSentenceIso x' =
+  let xMod = LBS.length x' `mod` 4
+      x = if xMod /= 0
+            then LBS.drop xMod x'
+            else x'
+  in  fmap Z85.decode (Z85.encode x) === Right (Right x)
+
+
+encodeSentenceIso' :: LBS.ByteString -> Property
+encodeSentenceIso' x' =
+  let xMod = LBS.length x' `mod` 4
+      x = if xMod /= 0
+            then LBS.drop xMod x'
+            else x'
+  in  fmap Z85.decode' (Z85.encode' x) === Right (Right x)
diff --git a/z85.cabal b/z85.cabal
new file mode 100644
--- /dev/null
+++ b/z85.cabal
@@ -0,0 +1,78 @@
+-- This file has been generated from package.yaml by hpack version 0.21.2.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: ed975b60096b74d143e23e35a8665dd16a714f3bf9474c5879a5daa40c07abea
+
+name:           z85
+version:        0.0.0
+synopsis:       Implementation of the z85 binary codec
+description:    Please see the README on GitHub at <https://github.com/githubuser/z85-bytestring#readme>
+category:       Data
+homepage:       https://github.com/athanclark/z85#readme
+bug-reports:    https://github.com/athanclark/z85/issues
+author:         Athan Clark
+maintainer:     athan.clark@localcooking.com
+copyright:      Copyright (c) 2018 Athan Clark
+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/athanclark/z85
+
+library
+  exposed-modules:
+      Data.Attoparsec.ByteString.Z85
+      Data.Attoparsec.Text.Z85
+      Data.ByteString.Z85
+      Data.ByteString.Z85.Internal
+      Pipes.Z85
+  other-modules:
+      Paths_z85
+  hs-source-dirs:
+      src
+  build-depends:
+      QuickCheck
+    , attoparsec
+    , attoparsec-binary
+    , base >=4.7 && <5
+    , bytestring
+    , pipes
+    , pipes-bytestring
+    , pipes-text
+    , text
+    , vector-sized
+  default-language: Haskell2010
+
+test-suite z85-bytestring-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Paths_z85
+  hs-source-dirs:
+      test
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      QuickCheck
+    , attoparsec
+    , attoparsec-binary
+    , base >=4.7 && <5
+    , bytestring
+    , pipes
+    , pipes-bytestring
+    , pipes-text
+    , quickcheck-instances
+    , tasty
+    , tasty-hunit
+    , tasty-quickcheck
+    , text
+    , vector-sized
+    , z85
+  default-language: Haskell2010
