diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,24 @@
+Copyright (c) 2013, ini
+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 ini 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 <COPYRIGHT HOLDER> 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/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/ini.cabal b/ini.cabal
new file mode 100644
--- /dev/null
+++ b/ini.cabal
@@ -0,0 +1,23 @@
+name:                ini
+version:             0.0.0
+synopsis:            Quick and easy configuration files in the INI format.
+description:         Simple, easy and fast configuration files in the INI format.
+license:             BSD3
+license-file:        LICENSE
+author:              Chris Done
+maintainer:          chrisdone@gmail.com
+copyright:           2013 Chris Done
+category:            Data, Configuration
+build-type:          Simple
+cabal-version:       >=1.8
+
+library
+  hs-source-dirs:    src/
+  ghc-options:       -Wall -O2
+  extensions:        OverloadedStrings
+  exposed-modules:   Data.Ini
+  build-depends:     base >= 4 && <5,
+                     attoparsec,
+                     text,
+                     aeson,
+                     unordered-containers
diff --git a/src/Data/Ini.hs b/src/Data/Ini.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Ini.hs
@@ -0,0 +1,120 @@
+-- | Clean configuration files in the INI format.
+--
+-- Format rules and recommendations:
+--
+--  * The @: @ syntax is space-sensitive.
+--
+--  * Keys are case-sensitive.
+--
+--  * Lower-case is recommended.
+--
+--  * Values can be empty.
+--
+--  * Keys cannot contain @:@, @=@, @[@, or @]@.
+--
+--  * Comments are not supported at this time.
+--
+-- An example configuration file:
+--
+-- @
+-- [SERVER]
+-- port=6667
+-- hostname=localhost
+-- [AUTH]
+-- user: hello
+-- pass: world
+-- salt:
+-- @
+--
+-- Parsing example:
+--
+-- >>> parseIni "[SERVER]\nport: 6667\nhostname: localhost"
+-- Right (Ini {unIni = fromList [("SERVER",fromList [("hostname","localhost"),("port","6667")])]})
+--
+
+module Data.Ini
+  (-- * Reading
+   parseIniFile
+  ,parseIni
+   -- * Writing
+  ,writeIniFile
+  ,printIni
+   -- * Types
+  ,Ini(..)
+   -- * Parsers
+  ,iniParser
+  ,sectionParser
+  ,keyValueParser
+  )
+  where
+
+import           Control.Monad
+import           Data.Attoparsec.Combinator
+import           Data.Attoparsec.Text
+import           Data.Char
+import           Data.HashMap.Strict (HashMap)
+import qualified Data.HashMap.Strict as M
+import           Data.Monoid
+import           Data.Text (Text)
+import qualified Data.Text as T
+import qualified Data.Text.IO as T
+import           Prelude hiding (takeWhile)
+
+-- | An INI configuration.
+newtype Ini = Ini { unIni :: HashMap Text (HashMap Text Text) }
+  deriving (Show)
+
+-- | Parse an INI file.
+parseIniFile :: FilePath -> IO (Either String Ini)
+parseIniFile = fmap parseIni . T.readFile
+
+-- | Parse an INI config.
+parseIni :: Text -> Either String Ini
+parseIni = parseOnly iniParser
+
+-- | Print the INI config to a file.
+writeIniFile :: FilePath -> Ini -> IO ()
+writeIniFile fp = T.writeFile fp . printIni
+
+-- | Print an INI config.
+printIni :: Ini -> Text
+printIni (Ini sections) =
+  T.concat (map buildSection (M.toList sections))
+  where buildSection (name,pairs) =
+          "[" <> name <> "]\n" <>
+          T.concat (map buildPair (M.toList pairs))
+        buildPair (name,value) =
+          name <> ": " <> value
+
+-- | Parser for an INI.
+iniParser :: Parser Ini
+iniParser = fmap Ini (fmap M.fromList (many1 sectionParser))
+
+-- | A section. Format: @[foo]@. Conventionally, @[FOO]@.
+sectionParser :: Parser (Text,HashMap Text Text)
+sectionParser =
+  do char '['
+     name <- takeWhile (\c -> c /=']' && c /= '[')
+     char ']'
+     skipEndOfLine
+     values <- many1 keyValueParser
+     return (name,M.fromList values)
+
+-- | A key-value pair. Either @foo: bar@ or @foo=bar@.
+keyValueParser :: Parser (Text,Text)
+keyValueParser =
+  do key <- takeWhile1 (\c -> not (isDelim c || c == '[' || c == ']'))
+     delim <- satisfy isDelim
+     value <- fmap (clean delim) (takeWhile (not . isEndOfLine))
+     skipEndOfLine
+     return (key,value)
+  where clean ':' = T.drop 1
+        clean _   = id
+
+-- | Is the given character a delimiter?
+isDelim :: Char -> Bool
+isDelim x = x == '=' || x == ':'
+
+-- | Skip end of line and whitespace beyond.
+skipEndOfLine :: Parser ()
+skipEndOfLine = skipWhile (\c -> isEndOfLine c || isSpace c)
