diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Andrew Martin (c) 2017
+
+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 Andrew Martin 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,1 @@
+# dhcp-lease-parser
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/dhcp-lease-parser.cabal b/dhcp-lease-parser.cabal
new file mode 100644
--- /dev/null
+++ b/dhcp-lease-parser.cabal
@@ -0,0 +1,45 @@
+name: dhcp-lease-parser
+version: 0.1
+homepage: https://github.com/andrewthad/dhcp-lease-parser#readme
+license: BSD3
+license-file: LICENSE
+author: Andrew Martin
+maintainer: andrew.thaddeus@gmail.com
+copyright: 2017 Andrew Martin
+category: software
+build-type: Simple
+extra-source-files: README.md
+cabal-version: >=1.10
+synopsis: Parse a DHCP lease file
+
+library
+  hs-source-dirs: src
+  exposed-modules:
+    Dhcp.Lease
+  build-depends:
+      base >= 4.7 && < 5
+    , attoparsec >= 0.13 && < 0.14
+    , text >= 1.2 && < 1.3
+    , bytestring >= 0.10 && < 0.11
+    , chronos >= 0.4 && < 0.5
+    , ip >= 0.9 && < 0.10
+  default-language: Haskell2010
+
+test-suite dhcp-lease-parser-test
+  type: exitcode-stdio-1.0
+  hs-source-dirs: test
+  main-is: Spec.hs
+  build-depends:
+      base
+    , tasty
+    , tasty-hunit
+    , attoparsec
+    , dhcp-lease-parser
+    , bytestring
+    , chronos
+    , ip
+  default-language: Haskell2010
+
+source-repository head
+  type:     git
+  location: https://github.com/andrewthad/dhcp-lease-parser
diff --git a/src/Dhcp/Lease.hs b/src/Dhcp/Lease.hs
new file mode 100644
--- /dev/null
+++ b/src/Dhcp/Lease.hs
@@ -0,0 +1,224 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE BangPatterns #-}
+module Dhcp.Lease
+  ( Lease(..)
+  , Value(..)
+  , Hardware(..)
+  , BindingState(..)
+  , parserLease
+  , parserLeases
+  , decodeLeases
+  ) where
+
+import qualified Data.Attoparsec.ByteString.Char8 as AB
+import qualified Data.Attoparsec.ByteString as ABB
+import qualified Data.Attoparsec.ByteString.Lazy as ALB
+import qualified Net.IPv4.ByteString.Char8 as IP
+import qualified Net.Mac.ByteString.Char8 as Mac
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Lazy as LB
+import qualified Data.ByteString.Char8 as BC
+import Data.Char (ord,chr)
+import Data.Text.Encoding (decodeUtf8')
+import Control.Applicative
+import Control.Monad
+import Data.Functor
+import Data.ByteString (ByteString)
+import Data.Text (Text)
+import Net.Types
+import Chronos.Datetime.ByteString.Char7 (parser_YmdHMS)
+import Chronos.Posix (fromDatetime)
+import Chronos.Types
+
+data Lease = Lease 
+  { leaseIp :: !IPv4
+  , leaseValues :: ![Value]
+  } deriving (Show,Read)
+
+data Value 
+  = ValueStarts !PosixTime
+  | ValueEnds !PosixTime
+  | ValueTstp !PosixTime
+  | ValueAtsfp !PosixTime
+  | ValueCltt !PosixTime
+  | ValueBindingState !BindingState
+  | ValueNextBindingState !BindingState
+  | ValueHardware !Hardware
+  | ValueUid !ByteString
+  | ValueClientHostname !Text
+  deriving (Eq,Ord,Show,Read)
+
+data NextValue = NextValuePresent !Value | NextValueAbsent
+data NextName = NextNamePresent !Name | NextNameAbsent
+
+data Name
+  = NameStarts
+  | NameEnds
+  | NameTstp
+  | NameAtsfp
+  | NameCltt
+  | NameBindingState
+  | NameNextBindingState
+  | NameHardware
+  | NameUid
+  | NameClientHostname
+
+data BindingState = BindingStateFree | BindingStateActive
+  deriving (Eq,Ord,Show,Read)
+data Hardware = Hardware
+  { hardwareType :: !Text
+  , hardwareMac :: !Mac
+  } deriving (Eq,Ord,Show,Read)
+
+decodeLeases :: LB.ByteString -> Maybe [Lease]
+decodeLeases = ALB.maybeResult . ALB.parse (parserLeases <* AB.endOfInput)
+
+-- | Parse as many leases as possible. Also,
+--   strip comments at the start of the input and between
+--   leases.
+parserLeases :: AB.Parser [Lease]
+parserLeases = go id
+  where
+  go :: ([Lease] -> [Lease]) -> AB.Parser [Lease]
+  go diffList = do
+    m <- AB.peekChar
+    case m of
+      Nothing -> return (diffList [])
+      Just c -> if c == '#'
+        then comment >> go diffList
+        else do
+          lease <- parserLease
+          go ((lease :) . diffList)
+
+comment :: AB.Parser ()
+comment = do
+  _ <- AB.takeTill (== '\n')
+  AB.skipSpace
+
+parserLease :: AB.Parser Lease
+parserLease = do
+  _ <- AB.string "lease"
+  AB.skipSpace
+  ip <- IP.parser
+  AB.skipSpace
+  _ <- AB.char '{'
+  AB.skipSpace
+  let go vs = do
+        nv <- parserValue
+        case nv of
+          NextValueAbsent -> return vs
+          NextValuePresent v -> go (v : vs)
+  vals <- go []
+  return (Lease ip vals)
+
+parserValue :: AB.Parser NextValue
+parserValue = do
+  nname <- (AB.string "starts" $> NextNamePresent NameStarts)
+    <|> (AB.string "ends" $> NextNamePresent NameEnds)
+    <|> (AB.string "tstp" $> NextNamePresent NameTstp)
+    <|> (AB.string "atsfp" $> NextNamePresent NameAtsfp)
+    <|> (AB.string "cltt" $> NextNamePresent NameCltt)
+    <|> (AB.string "binding state" $> NextNamePresent NameBindingState)
+    <|> (AB.string "next binding state" $> NextNamePresent NameNextBindingState)
+    <|> (AB.string "hardware" $> NextNamePresent NameHardware)
+    <|> (AB.string "uid" $> NextNamePresent NameUid)
+    <|> (AB.string "client-hostname" $> NextNamePresent NameClientHostname)
+    <|> (AB.char '}' $> NextNameAbsent)
+  case nname of
+    NextNameAbsent -> do
+      AB.skipSpace
+      return NextValueAbsent
+    NextNamePresent name -> do
+      AB.skipSpace
+      value <- case name of
+        NameStarts -> ValueStarts <$> parserTime
+        NameEnds -> ValueEnds <$> parserTime
+        NameTstp -> ValueTstp <$> parserTime
+        NameAtsfp -> ValueAtsfp <$> parserTime
+        NameCltt -> ValueCltt <$> parserTime
+        NameBindingState -> ValueBindingState <$> parserBindingState
+        NameNextBindingState -> ValueNextBindingState <$> parserBindingState
+        NameHardware -> ValueHardware <$> parserHardware
+        NameUid -> ValueUid <$> parserUid
+        NameClientHostname -> ValueClientHostname <$> parserClientHostname
+      AB.skipSpace
+      _ <- AB.char ';'
+      AB.skipSpace
+      return (NextValuePresent value)
+
+-- | This doesn't actually work yet. It doesn't espace octal codes.
+parserUid :: AB.Parser ByteString
+parserUid = do
+  isMac <- (AB.char '"' $> False ) <|> pure True
+  if isMac
+    then AB.takeTill (\x -> not (x == ':' || (x >= '0' && x <= '9') || (x >= 'a' && x <= 'f') || (x >= 'A' && x <= 'F')))
+    else do
+      let go !cs = do
+            n <- possiblyOctal
+            case n of
+              NextCharDone -> return cs
+              NextCharAgain c -> go (c : cs)
+      chars <- go []
+      return (B.reverse (BC.pack chars))
+
+octalErrorMessage :: String
+octalErrorMessage = "invalid octal escape sequence while parsing uid"
+
+c2i :: Char -> Int
+c2i = ord
+
+possiblyOctal :: AB.Parser NextChar
+possiblyOctal = do
+  c <- AB.anyChar
+  case c of
+    '"' -> return NextCharDone
+    '\\' -> do
+      c1 <- AB.anyChar
+      let i1 = c2i c1 - 48
+      when (i1 < 0 || i1 > 3) $ fail octalErrorMessage
+      c2 <- AB.anyChar
+      let i2 = c2i c2 - 48
+      when (i2 < 0 || i2 > 7) $ fail octalErrorMessage
+      c3 <- AB.anyChar
+      let i3 = c2i c3 - 48
+      when (i3 < 0 || i3 > 7) $ fail octalErrorMessage
+      return (NextCharAgain (chr (i1 * 64 + i2 * 8 + i3)))
+    _ -> return (NextCharAgain c)
+
+data NextChar = NextCharAgain {-# UNPACK #-} !Char | NextCharDone
+
+parserClientHostname :: AB.Parser Text
+parserClientHostname = do
+  _ <- AB.char '"'
+  bs <- AB.takeTill (== '"')
+  _ <- AB.anyChar
+  case decodeUtf8' bs of
+    Left _ -> fail "client hostname name not UTF-8"
+    Right name -> return name
+
+parserHardware :: AB.Parser Hardware
+parserHardware = Hardware
+  <$> (do
+        bs <- AB.takeWhile1 (/= ' ')
+        case decodeUtf8' bs of
+          Left _ -> fail "hardware name not UTF-8"
+          Right name -> return name
+      ) 
+  <*  AB.anyChar
+  <*> Mac.parserWith (MacCodec (MacGroupingPairs ':') False)
+
+parserBindingState :: AB.Parser BindingState
+parserBindingState =
+      (AB.string "active" $> BindingStateActive)
+  <|> (AB.string "free" $> BindingStateFree)
+
+parserTime :: AB.Parser PosixTime
+parserTime = do
+  _ <- AB.decimal :: AB.Parser Int
+  AB.skipSpace
+  dt <- parser_YmdHMS (DatetimeFormat (Just '/') (Just ' ') (Just ':'))
+  return (fromDatetime dt)
+  
+
+
+
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,81 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+import Test.Tasty
+import Test.Tasty.HUnit
+import Dhcp.Lease
+import Data.Monoid
+
+import qualified Data.List as L
+import qualified Data.ByteString as B
+import qualified Net.IPv4 as IPv4
+import qualified Net.Mac as Mac
+import qualified Chronos.Datetime as DT
+import qualified Chronos.Posix as PX
+import qualified Data.Attoparsec.ByteString as AB
+
+main :: IO ()
+main = defaultMain tests
+
+tests :: TestTree
+tests = testGroup "Tests" [unitTests]
+
+unitTests :: TestTree
+unitTests = testGroup "Unit tests"
+  [ testCase "000" $ oneLease "sample/000.txt" $ Lease
+      (IPv4.fromOctets 10 160 23 253)
+      [ ValueStarts $ PX.fromDatetime $ DT.fromYmdhms 2016 1 11 18 31 21
+      , ValueEnds $ PX.fromDatetime $ DT.fromYmdhms 2016 02 08 18 38 1
+      , ValueTstp $ PX.fromDatetime $ DT.fromYmdhms 2016 02 08 18 38 1
+      , ValueBindingState BindingStateFree
+      , ValueHardware $ Hardware "ethernet" $ Mac.fromOctets 0x00 0x02 0x80 0x05 0x3E 0x9A
+      ]
+  , testCase "001" $ oneLease "sample/001.txt" $ Lease
+      (IPv4.fromOctets 10 152 33 140)
+      [ ValueStarts $ PX.fromDatetime $ DT.fromYmdhms 2017 5 18 14 05 18
+      , ValueEnds $ PX.fromDatetime $ DT.fromYmdhms 2017 06 15 14 11 58
+      , ValueBindingState BindingStateActive
+      , ValueNextBindingState BindingStateFree
+      , ValueHardware $ Hardware "ethernet" $ Mac.fromOctets 0x00 0x02 0x80 0x04 0x30 0x63
+      , ValueUid $ "\o001\o000\o002\o200\o004" <> "0c"
+      , ValueClientHostname "000280043063"
+      ]
+  , testCase "002" $ oneLease "sample/002.txt" $ Lease
+      (IPv4.fromOctets 10 102 18 226)
+      [ ValueStarts $ PX.fromDatetime $ DT.fromYmdhms 2017 5 18 5 40 43
+      , ValueEnds $ PX.fromDatetime $ DT.fromYmdhms 2017 05 19 0 40 43
+      , ValueBindingState BindingStateActive
+      , ValueNextBindingState BindingStateFree
+      , ValueHardware $ Hardware "ethernet" $ Mac.fromOctets 0x00 0x02 0xA1 0x23 0x03 0x80
+      , ValueUid "\o001\o000\o002\o241#\o003\o200"
+      ]
+  , testCase "003" $ manyLeases "sample/003.txt" 3
+  ]
+
+oneLease :: String -> Lease -> Assertion
+oneLease filename expected = do
+  bs <- B.readFile filename
+  case AB.parseOnly (parserLease <* AB.endOfInput) bs of
+    Left err -> fail ("parse failed with: " ++ err)
+    Right actual -> assertEqual "Lease Equality" (LeaseWrap expected) (LeaseWrap actual)
+
+manyLeases ::
+     String -- ^ Filename
+  -> Int -- ^ expected number of leases
+  -> Assertion
+manyLeases filename expectedLen = do
+  bs <- B.readFile filename
+  case AB.parseOnly (parserLeases <* AB.endOfInput) bs of
+    Left err -> fail ("parse failed with: " ++ err)
+    Right actual -> assertEqual "Lease List Length Equality"
+      expectedLen (L.length actual)
+
+newtype LeaseWrap = LeaseWrap Lease
+  deriving (Show)
+
+instance Eq LeaseWrap where
+  LeaseWrap a == LeaseWrap b = leaseEq a b
+
+leaseEq :: Lease -> Lease -> Bool
+leaseEq (Lease ipA valsA) (Lease ipB valsB) =
+  ipA == ipB && L.sort valsA == L.sort valsB
+
