diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for snappy-hs
+
+## 0.1.0.0 -- YYYY-mm-dd
+
+* First version. Released on an unsuspecting world.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2025 Michael Chavinda
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,15 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Main (main) where
+
+import Snappy (decompress)
+
+import qualified Data.ByteString as BS
+
+main :: IO ()
+main = do
+  src <- BS.readFile "./data/Isaac.Newton-Opticks.txt.rawsnappy"
+  raw <- BS.readFile "./data/Isaac.Newton-Opticks.txt"
+  case decompress src of
+    Left e -> print e
+    Right decompressed -> do
+      print (decompressed == raw)
diff --git a/snappy-hs.cabal b/snappy-hs.cabal
new file mode 100644
--- /dev/null
+++ b/snappy-hs.cabal
@@ -0,0 +1,61 @@
+cabal-version:      3.0
+name:               snappy-hs
+version:            0.1.0.0
+synopsis:           A pure Haskell implementation of the Snappy compression spec.
+description:        A pure Haskell implementation of the Snappy compression spec.
+license:            MIT
+license-file:       LICENSE
+author:             Michael Chavinda
+maintainer:         mschavinda@gmail.com
+-- copyright:
+category:           Codec
+build-type:         Simple
+extra-doc-files:    CHANGELOG.md
+-- extra-source-files:
+
+common warnings
+    ghc-options: -Wall
+
+library
+    import:           warnings
+    exposed-modules:  Snappy
+
+    build-depends:    base >=4.11 && <5
+                    , bytestring ^>= 0.12
+                    , vector ^>= 0.13
+    hs-source-dirs:   src
+    default-language: Haskell2010
+
+executable snappy-hs
+    import:           warnings
+    main-is:          Main.hs
+    build-depends:
+        base >=4.11 && <5,
+        bytestring ^>= 0.12,
+        snappy-hs
+
+    hs-source-dirs:   app
+    default-language: Haskell2010
+
+test-suite snappy-hs-test
+    import:           warnings
+    default-language: Haskell2010
+    -- other-modules:
+    -- other-extensions:
+    type:             exitcode-stdio-1.0
+    hs-source-dirs:   test
+    main-is:          Main.hs
+    build-depends:
+        base >=4.11 && <5,
+        snappy-hs
+
+-- benchmark snappy-benchmark
+--     type:       exitcode-stdio-1.0
+--     main-is:    Main.hs
+--     hs-source-dirs: benchmark
+--     build-depends: base >= 4.17.2.0 && < 4.22,
+--                    criterion >= 1 && <= 1.6.4.0,
+--                    bytestring ^>= 0.12,
+--                    snappy ^>= 0.2,
+--                    snappy-hs
+--     default-language: Haskell2010
diff --git a/src/Snappy.hs b/src/Snappy.hs
new file mode 100644
--- /dev/null
+++ b/src/Snappy.hs
@@ -0,0 +1,290 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE Strict       #-}
+module Snappy (
+    compress,
+    decompress
+) where
+
+import           Control.Exception (Exception)
+import           Control.Monad.ST (ST, runST)
+import           Data.Bits ((.&.), (.|.), (.<<.), (.>>.))
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Builder as Builder
+import qualified Data.ByteString.Lazy as LBS
+import           Data.Word
+import qualified Data.Vector.Unboxed.Mutable as VUM
+
+decompress :: BS.ByteString -> Either ParseException BS.ByteString
+decompress s = do
+    (_, bytesRead) <- decodedLength s
+    let payload = BS.drop bytesRead s
+    decode payload
+
+maxVarintLen64 :: Int
+maxVarintLen64 = 10
+
+data ParseException = EmptyInput
+                    | Overflow
+                    | VarIntTooLong 
+                    | UnexpectedEOF
+                    | UnsupportedLiteralLength
+                    | InvalidOffset
+                    | InvalidTag deriving (Show, Eq)
+instance Exception ParseException
+
+decodedLength :: BS.ByteString -> Either ParseException (Int, Int)
+decodedLength src = do
+    (value, bytesRead) <- decodeVarInt src
+    pure (fromIntegral value, bytesRead)
+  where
+    decodeVarInt :: BS.ByteString -> Either ParseException (Word64, Int)
+    decodeVarInt bs
+        | BS.null bs = Left EmptyInput
+        | otherwise = go 0 0 0 bs
+      where
+        go acc shift bytesRead bytes
+            | bytesRead > maxVarintLen64 = Left VarIntTooLong
+            | shift > 63                 = Left Overflow
+            | otherwise                  = case BS.uncons bytes of
+                Nothing -> Left UnexpectedEOF
+                Just (b, rest)
+                    | b < 0x80 ->
+                        Right (acc .|. (fromIntegral b .<<. shift), bytesRead + 1)
+                    | otherwise ->
+                        go (acc .|. ((fromIntegral b .&. 0x7f) .<<. shift))
+                           (shift + 7)
+                           (bytesRead + 1)
+                           rest
+
+decode :: BS.ByteString -> Either ParseException BS.ByteString
+decode = go mempty BS.empty
+  where
+    go :: Builder.Builder -> BS.ByteString -> BS.ByteString -> Either ParseException BS.ByteString
+    go !acc !out bs
+      | BS.null bs = Right (toStrict acc)
+      | otherwise  = case BS.uncons bs of
+          Nothing -> Left UnexpectedEOF
+          Just (tag, rest) ->
+            case tag .&. 0x03 of
+              0 -> do
+                let hdr = fromIntegral (tag .>>. 2) :: Int
+                if hdr < 60
+                  then do
+                    let len = hdr + 1
+                    (lit, rest') <- takeN len rest
+                    let acc' = acc <> Builder.byteString lit
+                        out' = out <> lit
+                    go acc' out' rest'
+                  else do
+                    let nbytes = hdr - 59
+                    whenBad (nbytes < 1 || nbytes > 4) UnsupportedLiteralLength
+                    (lenMinus1, rest1) <- readLE nbytes rest
+                    let len = lenMinus1 + 1
+                    (lit, rest') <- takeN len rest1
+                    let acc' = acc <> Builder.byteString lit
+                        out' = out <> lit
+                    go acc' out' rest'
+
+              1 -> do
+                (lo, rest1) <- take1 rest
+                let len    = (fromIntegral ((tag .>>. 2) .&. 0x07) :: Int) + 4
+                    offHi  = fromIntegral (tag .>>. 5) :: Int
+                    offset = (offHi .<<. 8) .|. fromIntegral lo
+                chunk <- makeCopy out offset len
+                let acc' = acc <> Builder.byteString chunk
+                    out' = out <> chunk
+                go acc' out' rest1
+
+              2 -> do
+                (off, rest1) <- readLE 2 rest
+                let len = (fromIntegral ((tag .>>. 2) .&. 0x3f) :: Int) + 1
+                chunk <- makeCopy out off len
+                let acc' = acc <> Builder.byteString chunk
+                    out' = out <> chunk
+                go acc' out' rest1
+
+              3 -> do
+                (off, rest1) <- readLE 4 rest
+                let len = (fromIntegral ((tag .>>. 2) .&. 0x3f) :: Int) + 1
+                chunk <- makeCopy out off len
+                let acc' = acc <> Builder.byteString chunk
+                    out' = out <> chunk
+                go acc' out' rest1
+              _ -> Left InvalidTag
+
+toStrict :: Builder.Builder -> BS.ByteString
+toStrict = LBS.toStrict . Builder.toLazyByteString
+
+take1 :: BS.ByteString -> Either ParseException (Word8, BS.ByteString)
+take1 bs = maybe (Left UnexpectedEOF) Right (BS.uncons bs)
+
+takeN :: Int -> BS.ByteString -> Either ParseException (BS.ByteString, BS.ByteString)
+takeN n bs
+    | BS.length bs < n = Left UnexpectedEOF
+    | otherwise        = Right (BS.take n bs, BS.drop n bs)
+
+readLE :: Int -> BS.ByteString -> Either ParseException (Int, BS.ByteString)
+readLE n bs = do
+    (bytes, rest) <- takeN n bs
+    let val = leBytesToInt bytes
+    pure (val, rest)
+
+leBytesToInt :: BS.ByteString -> Int
+leBytesToInt =
+    snd . BS.foldl' (\(!i, !acc) b -> (i+8, acc .|. (fromIntegral b .<<. i))) (0, 0)
+
+whenBad :: Bool -> ParseException -> Either ParseException ()
+whenBad True  e = Left e
+whenBad False _ = Right ()
+
+makeCopy :: BS.ByteString -> Int -> Int -> Either ParseException BS.ByteString
+makeCopy out offset len = do
+    whenBad (offset <= 0) InvalidOffset
+    let outLen = BS.length out
+    whenBad (offset > outLen) InvalidOffset
+    let start   = outLen - offset
+        pattern = BS.take offset (BS.drop start out)
+    pure $ repeatToLen pattern len
+
+repeatToLen :: BS.ByteString -> Int -> BS.ByteString
+repeatToLen pat n =
+    let plen   = BS.length pat
+        times  = n `div` plen
+        remain = n `mod` plen
+        bld    = mconcat (replicate times (Builder.byteString pat))
+                <> Builder.byteString (BS.take remain pat)
+    in LBS.toStrict (Builder.toLazyByteString bld)
+
+compress :: BS.ByteString -> BS.ByteString
+compress src =
+  let header  = Builder.toLazyByteString (putVarint (fromIntegral (BS.length src)))
+      payload = Builder.toLazyByteString (encodePayload src)
+  in LBS.toStrict (header <> payload)
+
+encodePayload :: BS.ByteString -> Builder.Builder
+encodePayload bs
+  | n < 4     = emitLiteral bs 0 n
+  | otherwise = runST $ do
+      let tableSize = 1 .<<. 15
+      tbl <- VUM.replicate tableSize (-1 :: Int)
+      loop tbl 0 0 mempty
+  where
+    n = BS.length bs
+
+    loop :: VUM.MVector s Int -> Int -> Int -> Builder.Builder -> ST s Builder.Builder
+    loop tbl !i !litStart !acc
+      | i + 3 >= n = pure (acc <> emitLiteral bs litStart (n - litStart))
+      | otherwise = do
+          let !w = readU32LE bs i
+              !h = hash32 w
+          prev <- VUM.read tbl h
+          VUM.write tbl h i
+          case prev of
+            (-1) -> loop tbl (i+1) litStart acc
+            p    -> do
+              let off = i - p
+              if off <= 0 || p + 3 >= n
+                 then loop tbl (i+1) litStart acc
+                 else if readU32LE bs p == w
+                       then do
+                         let m0   = 4 + extendMatch bs (i+4) (p+4) n
+                             acc' = if i > litStart
+                                      then acc <> emitLiteral bs litStart (i - litStart)
+                                      else acc
+                             acc'' = acc' <> emitCopies off m0
+                             i'    = i + m0
+                         loop tbl i' i' acc''
+                       else loop tbl (i+1) litStart acc
+
+extendMatch :: BS.ByteString -> Int -> Int -> Int -> Int
+extendMatch bs = go
+  where
+    go !a !b !n
+      | a < n && b < n && BS.index bs a == BS.index bs b = 1 + go (a+1) (b+1) n
+      | otherwise = 0
+
+hash32 :: Word32 -> Int
+hash32 w =
+  let kMul :: Word32
+      kMul = 0x1e35a7bd
+      bucketBits = 15
+  in fromIntegral ((w * kMul) .>>. (32 - bucketBits))
+
+readU32LE :: BS.ByteString -> Int -> Word32
+readU32LE s i =
+  let b0 = fromIntegral (BS.index s i    ) :: Word32
+      b1 = fromIntegral (BS.index s (i+1)) :: Word32
+      b2 = fromIntegral (BS.index s (i+2)) :: Word32
+      b3 = fromIntegral (BS.index s (i+3)) :: Word32
+  in b0 .|. (b1 .<<. 8) .|. (b2 .<<. 16) .|. (b3 .<<. 24)
+
+putVarint :: Word64 -> Builder.Builder
+putVarint x0 = go x0
+  where
+    go !x
+      | x < 0x80  = Builder.word8 (fromIntegral x)
+      | otherwise = Builder.word8 (fromIntegral (0x80 .|. (x .&. 0x7f)))
+                   <> go (x .>>. 7)
+
+emitLiteral :: BS.ByteString -> Int -> Int -> Builder.Builder
+emitLiteral _  _  0 = mempty
+emitLiteral bs off len
+  | len <= 60 =
+      let tag = fromIntegral (((len - 1) .<<. 2) .|. 0x00) :: Word8
+      in Builder.word8 tag <> Builder.byteString (bsSlice bs off len)
+  | otherwise =
+      let l1       = len - 1
+          nbytes   = bytesFor l1
+          tag      = fromIntegral (((59 + nbytes) .<<. 2) .|. 0x00) :: Word8
+      in  Builder.word8 tag
+       <> putLE nbytes (fromIntegral l1 :: Word32)
+       <> Builder.byteString (bsSlice bs off len)
+
+emitCopies :: Int -> Int -> Builder.Builder
+emitCopies !offset !len
+  | len <= 0 = mempty
+  | otherwise =
+      let (chunk, rest) =
+            if offset <= 0x7FF && len >= 4 then
+              let c = min 11 len
+              in (emitCopy1 c offset, len - c)
+            else if offset <= 0xFFFF then
+              let c = min 64 len
+              in (emitCopy2 c offset, len - c)
+            else
+              let c = min 64 len
+              in (emitCopy4 c offset, len - c)
+      in chunk <> emitCopies offset rest
+
+emitCopy1 :: Int -> Int -> Builder.Builder
+emitCopy1 len off =
+  let lenField = (len - 4) .&. 0x7
+      offHi    = (off .>>. 8) .&. 0x7
+      tag      = fromIntegral ((lenField .<<. 2) .|. (offHi .<<. 5) .|. 0x01) :: Word8
+      lo       = fromIntegral (off .&. 0xFF) :: Word8
+  in Builder.word8 tag <> Builder.word8 lo
+
+emitCopy2 :: Int -> Int -> Builder.Builder
+emitCopy2 len off =
+  let tag = fromIntegral (((len - 1) .<<. 2) .|. 0x02) :: Word8
+  in  Builder.word8 tag
+   <> putLE 2 (fromIntegral off :: Word32)
+
+emitCopy4 :: Int -> Int -> Builder.Builder
+emitCopy4 len off =
+  let tag = fromIntegral (((len - 1) .<<. 2) .|. 0x03) :: Word8
+  in  Builder.word8 tag
+   <> putLE 4 (fromIntegral off :: Word32)
+
+putLE :: Int -> Word32 -> Builder.Builder
+putLE n v = mconcat [ Builder.word8 (fromIntegral ((v .>>. (8*k)) .&. 0xFF)) | k <- [0..n-1] ]
+
+bytesFor :: Int -> Int
+bytesFor x
+  | x < 0x100      = 1
+  | x < 0x10000    = 2
+  | x < 0x1000000  = 3
+  | otherwise      = 4
+
+bsSlice :: BS.ByteString -> Int -> Int -> BS.ByteString
+bsSlice s off len = BS.take len (BS.drop off s)
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,4 @@
+module Main (main) where
+
+main :: IO ()
+main = putStrLn "Test suite not yet implemented."
