diff --git a/.gitignore b/.gitignore
new file mode 100644
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,1 @@
+/dist/
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,28 @@
+Copyright (c) 2003-2009, John Wiegley.  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 New Artisans LLC nor the names of its
+  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,1 @@
+A Haskell library for reading Subversion dump files.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,8 @@
+#!/usr/bin/runhaskell
+
+module Main (main) where
+
+import Distribution.Simple
+
+main :: IO ()
+main = defaultMain
diff --git a/src/Subversion.hs b/src/Subversion.hs
new file mode 100644
--- /dev/null
+++ b/src/Subversion.hs
@@ -0,0 +1,5 @@
+module Subversion
+  ( module Subversion.Dump
+  ) where
+
+import Subversion.Dump
diff --git a/src/Subversion/Dump.hs b/src/Subversion/Dump.hs
new file mode 100644
--- /dev/null
+++ b/src/Subversion/Dump.hs
@@ -0,0 +1,223 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Subversion.Dump
+       ( RevDate
+       , Revision(..)
+
+       , OpKind(..)
+       , OpAction(..)
+       , Operation(..)
+
+       , FieldMap
+       , Entry(..)
+
+       , readSvnDumpRaw
+       , readSvnDump
+       ) where
+
+{-| This is a parser for Subversion dump files.  The objective is to convert a
+dump file into a series of data structures representing that same information.
+It uses `Data.ByteString.Lazy` to reading the file, and `Data.Text` to
+represent text fields which may contain Unicode characters. -}
+
+--import Debug.Trace
+import           Control.Applicative hiding (many, (<|>))
+import           Control.Monad
+import qualified Data.ByteString.Lazy as B
+--import qualified Data.ByteString.Lazy.Char8 as BC
+import qualified Data.List as L
+--import qualified Data.Map as M
+import           Data.Maybe
+import           Data.Text.Lazy hiding (map, count)
+import           Data.Text.Lazy.Encoding as E
+import           System.FilePath
+import           Text.Parsec
+import           Text.Parsec.ByteString.Lazy as PB
+
+import           Prelude hiding (getContents)
+
+default (Data.Text.Lazy.Text)
+
+{- At the topmost level, a dump file is simple an in-order, linear list of
+revisions, where each revisions consist of a series of "operation nodes" that
+represent the changes made by that revision to the repository.  The author
+name and revision comment are decoded from UTF8. -}
+
+type RevDate = Text
+
+data Revision = Revision { revNumber     :: Int
+                         , revDate       :: RevDate
+                         , revAuthor     :: Maybe Text
+                         , revComment    :: Maybe Text
+                         , revOperations :: [Operation] }
+              deriving Show
+
+{- Each node reflects the changes to a single file.  Note that branches don't
+need to be considered separately, since in Subversion, all files are stored
+within a single filesystem.  Branches are something the user applies "after
+the fact" by using specially named paths, such as "foo/branches".  The file's
+contents are not decoded, as we have no way of knowing what the intended
+encoding should be -- or even if there is in, in the case of binary files.
+
+`opContentLength` is provided as a separate member to avoid reading in the
+full contents of the operation solely to determine its length.  This way, you
+can inspect the length while deferring the content read if you don't need
+it. -}
+
+data OpKind   = File | Directory deriving (Show, Enum, Eq)
+data OpAction = Add | Change | Replace | Delete deriving (Show, Enum, Eq)
+
+data Operation = Operation { opKind          :: OpKind
+                           , opAction        :: OpAction
+                           , opPathname      :: FilePath
+                           , opContents      :: B.ByteString
+                           , opContentLength :: Int
+                           , opChecksumMD5   :: Maybe String
+                           , opChecksumSHA1  :: Maybe String
+                           , opCopyFromRev   :: Maybe Int
+                           , opCopyFromPath  :: Maybe FilePath }
+               deriving Show
+
+{- A further note is needed on `opCopyFromRev` and `opCopyFromPath`, since these
+two represent the only real complexity in a dump file.  Basically what they
+say is that there is no `opContents` record for this `Operation`.  Rather, the
+contents to be taken from another file in a past revision.  Since this
+historical information would be expensive to maintain, `Operation` only
+provides the data given by the dump file, and it is left as an analytical pass
+on this data to build the structures necessary to figure out what those
+contents would have been.
+
+So, with our structures defined, we're ready to read in the file.  Since we
+don't know what each element will be yet (revisions are interspersed with
+nodes), we read them first into the much more general Node structure. -}
+
+{-| Reads a dump file from a ByteString in the IO monad into a list of
+    Revision values.  This is the "cooked" parallel of `readSvnDumpRaw`. -}
+readSvnDump :: B.ByteString -> IO (Either ParseError [Revision])
+readSvnDump io = do
+  result <- readSvnDumpRaw io
+  return $ map processRevs <$> (L.groupBy sameRev <$> result)
+
+  where sameRev _ y     = isNothing $
+                          L.lookup "Revision-number" (entryTags y)
+        getField f n x  = L.lookup n (f x)
+        getField' f n x = fromMaybe "" (getField f n x)
+        tagM            = getField entryTags
+        propM           = getField entryProps
+        tag             = getField' entryTags
+        prop            = getField' entryProps
+
+        processRevs [] = error "Unexpected"
+        processRevs (rev:ops) =
+          Revision {
+              revNumber     = read $ tag "Revision-number" rev
+            , revDate       = parseDate $ prop "svn:date" rev
+            , revAuthor     = propM "svn:author" rev
+            , revComment    = propM "svn:log" rev
+            , revOperations = map processOp ops }
+
+        processOp op =
+          Operation {
+              opKind          = getOpKind $ tag "Node-kind" op
+            , opAction        = getOpAction $ tag "Node-action" op
+            , opPathname      = tag "Node-path" op
+            , opContents      = entryBody op
+            , opContentLength = read $ tag "Text-content-length" op
+            , opCopyFromRev   = read <$>
+                                tagM "Node-copyfrom-rev" op
+            , opCopyFromPath  = tagM "Node-copyfrom-path" op
+            , opChecksumMD5   = tagM "Text-content-md5" op
+            , opChecksumSHA1  = tagM "Text-content-sha1" op }
+
+        getOpKind kind = case kind of
+          "file" -> File
+          "dir"  -> Directory
+          _      -> error "Unexpected"
+
+        getOpAction kind = case kind of
+          "add"     -> Add
+          "delete"  -> Delete
+          "change"  -> Change
+          "replace" -> Replace
+          _      -> error "Unexpected"
+
+type FieldMap a = [(String, a)]
+
+data Entry = Entry { entryTags  :: FieldMap String
+                   , entryProps :: FieldMap Text
+                   , entryBody  :: B.ByteString }
+             deriving Show
+
+readSvnDumpRaw :: B.ByteString -> IO (Either ParseError [Entry])
+readSvnDumpRaw dump = return $ parse parseSvnDump "" dump
+
+{- These are the Parsec parsers for the various parts of the input file. -}
+
+parseTag :: PB.Parser (String, String)
+parseTag = (,) <$> fieldKey   <* char ':' <* space
+               <*> fieldValue <* newline
+  where
+    fieldKey   = (:) <$> letter <*> many fieldChar
+    fieldChar  = letter <|> digit <|> oneOf "-_"
+    fieldValue = many1 (noneOf "\n")
+
+parseIndicator :: PB.Parser (Char, Integer)
+parseIndicator = (,) <$> oneOf "KV" <* space
+                     <*> (read <$> many1 digit <* newline)
+
+readTextRange :: Integer -> PB.Parser B.ByteString
+readTextRange len = do
+  input <- getInput
+  let value = B.take (fromIntegral len) input
+  setInput $ B.drop (fromIntegral len) input
+  return value
+
+--readTextRange' :: Integer -> PB.Parser B.ByteString
+--readTextRange' len = BC.pack <$> count (fromIntegral len) anyChar
+
+parseSpecValue :: Char -> PB.Parser Text
+parseSpecValue expected = do
+  (kind, len) <- parseIndicator
+  when (kind /= expected) $ unexpected "Unexpected spec value char"
+  value <- readTextRange len
+  --trace ("Value: " ++ (show value)) $ return ()
+  _ <- newline
+  return $ E.decodeUtf8 value
+
+parseProperty :: PB.Parser (String, Text)
+parseProperty = (,) <$> (unpack <$> parseSpecValue 'K')
+                    <*> parseSpecValue 'V'
+
+parseEntry :: PB.Parser Entry
+parseEntry = do
+  fields <- many1 parseTag <* newline
+
+  props  <- case L.lookup "Prop-content-length" fields of
+              Nothing -> return []
+              Just _  -> many parseProperty <* string "PROPS-END\n"
+
+  body   <- case L.lookup "Text-content-length" fields of
+              Nothing  -> return B.empty
+              Just len -> readTextRange (read len)
+
+  _ <- many newline <?> "entry-terminating newline"
+
+  return Entry { entryTags  = fields
+               , entryProps = props
+               , entryBody  = body }
+
+parseHeader :: PB.Parser ()
+parseHeader = do
+  _ <- string "SVN-fs-dump-format-version: 2\n\n"
+       <?> "Dump file starts without a recognizable tag"
+  _ <- string "UUID: " <* many1 (hexDigit <|> char '-')
+       <* newline <* newline
+  return ()
+
+parseSvnDump :: PB.Parser [Entry]
+parseSvnDump = parseHeader >> many parseEntry
+
+parseDate :: Text -> RevDate
+parseDate = id
+
+-- SvnDump.hs ends here
diff --git a/svndump.cabal b/svndump.cabal
new file mode 100644
--- /dev/null
+++ b/svndump.cabal
@@ -0,0 +1,72 @@
+name:          svndump
+category:      Subversion
+version:       0.1.0
+license:       BSD3
+cabal-version: >= 1.8
+license-file:  LICENSE
+author:        John Wiegley
+maintainer:    John Wiegley <johnw@newartisans.com>
+stability:     provisional
+homepage:      http://github.com/jwiegley/svndump/
+bug-reports:   http://github.com/jwiegley/svndump/issues
+copyright:     Copyright (C) 2012 John Wiegley
+synopsis:      Library for reading Subversion dump files
+description:
+  A library for parsing Subversion dump files.  The objective is to convert a
+  dump file into a series of data structures representing that same
+  information.  It uses `Data.ByteString.Lazy` to reading the file, and
+  `Data.Text` to represent text fields which may contain Unicode characters.
+
+build-type:    Simple
+tested-with:   GHC == 7.4.2
+extra-source-files:
+  .gitignore
+  LICENSE
+  README.md
+
+source-repository head
+  type: git
+  location: git://github.com/jwiegley/svndump.git
+
+library
+  build-depends:
+    base                 >= 4.3      && < 5,
+    parsec               >= 3.1.3,
+    filepath             >= 1.3,
+    bytestring           >= 0.9      && < 0.10,
+    text                 >= 0.11     && < 0.12
+
+  exposed-modules:
+    Subversion
+    Subversion.Dump
+
+  ghc-options: -Wall -fwarn-tabs -O2
+  hs-source-dirs: src
+
+-- Test the raw dump file parser
+test-suite test-raw
+  type:    exitcode-stdio-1.0
+  main-is: test-raw.hs
+
+  build-depends:
+    base                 >= 4.3      && < 5,
+    bytestring           >= 0.9      && < 0.10,
+    zlib                 >= 0.5      && < 0.6,
+    svndump
+
+  ghc-options: -Wall -Werror -O2
+  hs-source-dirs: test
+
+-- Test the raw dump file parser
+test-suite test-cooked
+  type:    exitcode-stdio-1.0
+  main-is: test-cooked.hs
+
+  build-depends:
+    base                 >= 4.3      && < 5,
+    bytestring           >= 0.9      && < 0.10,
+    zlib                 >= 0.5      && < 0.6,
+    svndump
+
+  ghc-options: -Wall -Werror -O2
+  hs-source-dirs: test
diff --git a/test/test-cooked.hs b/test/test-cooked.hs
new file mode 100644
--- /dev/null
+++ b/test/test-cooked.hs
@@ -0,0 +1,21 @@
+module Main where
+
+import qualified Codec.Compression.GZip as GZip
+import qualified Data.ByteString.Lazy as B
+import           Subversion.Dump
+import           System.Exit
+
+main :: IO ()
+main = do
+  file <- B.readFile "data/cunit.dump.gz"
+  dump <- readSvnDump $ GZip.decompress file
+  case dump of
+    Left _   -> exitFailure
+    Right xs -> do
+      let len = length xs
+      putStrLn $ show len ++ " cooked entries found, expecting 157"
+      if len == 157
+        then exitSuccess
+        else exitFailure
+
+-- raw-parser.hs ends here
diff --git a/test/test-raw.hs b/test/test-raw.hs
new file mode 100644
--- /dev/null
+++ b/test/test-raw.hs
@@ -0,0 +1,21 @@
+module Main where
+
+import qualified Codec.Compression.GZip as GZip
+import qualified Data.ByteString.Lazy as B
+import           Subversion.Dump
+import           System.Exit
+
+main :: IO ()
+main = do
+  file <- B.readFile "data/cunit.dump.gz"
+  dump <- readSvnDumpRaw $ GZip.decompress file
+  case dump of
+    Left _   -> exitFailure
+    Right xs -> do
+      let len = length xs
+      putStrLn $ show len ++ " raw entries found, expecting 1950"
+      if len == 1950
+        then exitSuccess
+        else exitFailure
+
+-- raw-parser.hs ends here
