diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,24 @@
+Copyright (c) 2015, FP Complete
+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 paths 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/path.cabal b/path.cabal
new file mode 100644
--- /dev/null
+++ b/path.cabal
@@ -0,0 +1,31 @@
+name:                path
+version:             0.0.0
+synopsis:            Path
+description:         Path
+license:             BSD3
+license-file:        LICENSE
+author:              Chris Done
+maintainer:          chrisdone@fpcomplete.com
+copyright:           2015 FP Complete
+category:            Filesystem
+build-type:          Simple
+cabal-version:       >=1.8
+
+library
+  hs-source-dirs:    src/
+  ghc-options:       -Wall -O2
+  exposed-modules:   Path, Path.Internal
+  build-depends:     base >= 4 && <5
+                   , exceptions
+                   , filepath
+                   , template-haskell
+
+test-suite test
+    type: exitcode-stdio-1.0
+    main-is: Main.hs
+    hs-source-dirs: test
+    build-depends: HUnit
+                 , base
+                 , hspec
+                 , mtl
+                 , path
diff --git a/src/Path.hs b/src/Path.hs
new file mode 100644
--- /dev/null
+++ b/src/Path.hs
@@ -0,0 +1,275 @@
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE EmptyDataDecls #-}
+
+-- | A normalizing well-typed path type.
+
+module Path
+  (-- * Types
+   Path
+  ,Abs
+  ,Rel
+  ,File
+  ,Dir
+   -- * Parsing
+  ,parseAbsDir
+  ,parseRelDir
+  ,parseAbsFile
+  ,parseRelFile
+  ,PathParseException
+  -- * Constructors
+  ,mkAbsDir
+  ,mkRelDir
+  ,mkAbsFile
+  ,mkRelFile
+  -- * Operations
+  ,(</>)
+  ,stripDir
+  ,isParentOf
+  ,parentAbs
+  ,filename
+  -- * Conversion
+  ,toFilePath
+  )
+  where
+
+import           Control.Exception (Exception)
+import           Control.Monad.Catch (MonadThrow(..))
+import           Data.Data
+import           Data.List
+import           Data.Maybe
+import           Language.Haskell.TH
+import           Path.Internal
+import qualified System.FilePath as FilePath
+
+--------------------------------------------------------------------------------
+-- Types
+
+-- | An absolute path.
+data Abs
+
+-- | A relative path; one without a root.
+data Rel
+
+-- | A file path.
+data File
+
+-- | A directory path.
+data Dir
+
+-- | Exception when parsing a location.
+data PathParseException
+  = InvalidAbsDir FilePath
+  | InvalidRelDir FilePath
+  | InvalidAbsFile FilePath
+  | InvalidRelFile FilePath
+  deriving (Show,Typeable)
+instance Exception PathParseException
+
+--------------------------------------------------------------------------------
+-- Parsers
+
+-- | Get a location for an absolute directory. Produces a normalized
+--  path which always ends in a path separator.
+--
+-- Throws: 'PathParseException'
+--
+parseAbsDir :: MonadThrow m
+            => FilePath -> m (Path Abs Dir)
+parseAbsDir filepath =
+  if FilePath.isAbsolute filepath &&
+     not (null (normalizeDir filepath)) &&
+     not (isPrefixOf "~/" filepath)
+     then return (Path (normalizeDir filepath))
+     else throwM (InvalidAbsDir filepath)
+
+-- | Get a location for a relative directory. Produces a normalized
+-- path which always ends in a path separator.
+--
+-- Throws: 'PathParseException'
+--
+parseRelDir :: MonadThrow m
+            => FilePath -> m (Path Rel Dir)
+parseRelDir filepath =
+  if not (FilePath.isAbsolute filepath) &&
+     not (null filepath) &&
+     not (isPrefixOf "~/" filepath) &&
+     not (null (normalizeDir filepath))
+     then return (Path (normalizeDir filepath))
+     else throwM (InvalidRelDir filepath)
+
+-- | Get a location for an absolute file. Produces a normalized
+--  path which always ends in a path separator.
+--
+-- Throws: 'PathParseException'
+--
+parseAbsFile :: MonadThrow m
+             => FilePath -> m (Path Abs File)
+parseAbsFile filepath =
+  if FilePath.isAbsolute filepath &&
+     not (FilePath.hasTrailingPathSeparator filepath) &&
+     not (isPrefixOf "~/" filepath) &&
+     not (null (normalizeFile filepath))
+     then return (Path (normalizeFile filepath))
+     else throwM (InvalidAbsFile filepath)
+
+-- | Get a location for a relative file. Produces a normalized
+-- path which always ends in a path separator.
+--
+-- Throws: 'PathParseException'
+--
+parseRelFile :: MonadThrow m
+             => FilePath -> m (Path Rel File)
+parseRelFile filepath =
+  if not (FilePath.isAbsolute filepath || FilePath.hasTrailingPathSeparator filepath) &&
+     not (null filepath) &&
+     not (isPrefixOf "~/" filepath) &&
+     not (null (normalizeFile filepath))
+     then return (Path (normalizeFile filepath))
+     else throwM (InvalidRelFile filepath)
+
+--------------------------------------------------------------------------------
+-- Constructors
+
+-- | Make a 'Path Abs Dir'.
+--
+-- Remember: due to the nature of absolute paths this (e.g. @\/home\/foo@)
+-- may compile on your platform, but it may not compile on another
+-- platform (Windows).
+mkAbsDir :: FilePath -> Q Exp
+mkAbsDir s =
+  case parseAbsDir s of
+    Left err -> error (show err)
+    Right (Path str) ->
+      [|Path $(return (LitE (StringL str))) :: Path Abs Dir|]
+
+-- | Make a 'Path Rel Dir'.
+mkRelDir :: FilePath -> Q Exp
+mkRelDir s =
+  case parseRelDir s of
+    Left err -> error (show err)
+    Right (Path str) ->
+      [|Path $(return (LitE (StringL str))) :: Path Rel Dir|]
+
+-- | Make a 'Path Abs File'.
+--
+-- Remember: due to the nature of absolute paths this (e.g. @\/home\/foo@)
+-- may compile on your platform, but it may not compile on another
+-- platform (Windows).
+mkAbsFile :: FilePath -> Q Exp
+mkAbsFile s =
+  case parseAbsFile s of
+    Left err -> error (show err)
+    Right (Path str) ->
+      [|Path $(return (LitE (StringL str))) :: Path Abs File|]
+
+-- | Make a 'Path Rel File'.
+mkRelFile :: FilePath -> Q Exp
+mkRelFile s =
+  case parseRelFile s of
+    Left err -> error (show err)
+    Right (Path str) ->
+      [|Path $(return (LitE (StringL str))) :: Path Rel File|]
+
+--------------------------------------------------------------------------------
+-- Conversion
+
+-- | Convert to a 'FilePath' type.
+toFilePath :: Path b t -> FilePath
+toFilePath (Path l) = l
+
+--------------------------------------------------------------------------------
+-- Operations
+
+-- | Append two paths.
+--
+-- The following cases are valid and the equalities hold:
+--
+-- @$(mkAbsDir x) \<\/> $(mkRelDir y) = $(mkAbsDir (x ++ \"/\" ++ y))@
+--
+-- @$(mkAbsDir x) \<\/> $(mkRelFile y) = $(mkAbsFile (x ++ \"/\" ++ y))@
+--
+-- @$(mkRelDir x) \<\/> $(mkRelDir y) = $(mkRelDir (x ++ \"/\" ++ y))@
+--
+-- @$(mkRelDir x) \<\/> $(mkRelFile y) = $(mkRelFile (x ++ \"/\" ++ y))@
+--
+-- The following are proven not possible to express:
+--
+-- @$(mkAbsFile …) \<\/> x@
+--
+-- @$(mkRelFile …) \<\/> x@
+--
+-- @x \<\/> $(mkAbsFile …)@
+--
+-- @x \<\/> $(mkAbsDir …)@
+--
+(</>) :: Path b Dir -> Path Rel t -> Path b t
+(</>) (Path a) (Path b) = Path (a ++ b)
+
+-- | Strip directory from path, making it relative to that directory.
+-- Returns 'Nothing' if directory is not a parent of the path.
+--
+-- The following properties hold:
+--
+-- @stripDir parent (parent <\/> child) = child@
+--
+-- Cases which are proven not possible:
+--
+-- @stripDir (a :: Path Abs …) (b :: Path Rel …)@
+--
+-- @stripDir (a :: Path Rel) (b :: Path Abs …)@
+--
+-- In other words the bases must match.
+--
+stripDir :: Path b Dir -> Path b t -> Maybe (Path Rel t)
+stripDir (Path p) (Path l) =
+  fmap Path (stripPrefix p l)
+
+-- | Is p a parent of the given location? Implemented in terms of
+-- 'stripDir'. The bases must match.
+isParentOf :: Path b Dir -> Path b t -> Bool
+isParentOf p l =
+  isJust (stripDir p l)
+
+-- | Take the absolute parent directory from the absolute path.
+--
+-- The following properties hold:
+--
+-- @parentAbs (parent \<\/> child) == parent@
+--
+-- On the root, getting the parent is idempotent:
+--
+-- @parentAbs (parentAbs \"\/\") = \"\/\"@
+--
+parentAbs :: Path Abs t -> Path Abs t
+parentAbs (Path fp) =
+  Path (normalizeDir (FilePath.takeDirectory (FilePath.dropTrailingPathSeparator fp)))
+
+-- | Extract the relative filename from a given location.
+--
+-- The following properties hold:
+--
+-- @filename (parent \<\/> filename a) == a@
+--
+filename :: Path b File -> Path Rel File
+filename (Path l) = Path (normalizeFile (FilePath.takeFileName l))
+
+--------------------------------------------------------------------------------
+-- Internal functions
+
+-- | Internal use for normalizing a directory.
+normalizeDir :: FilePath -> FilePath
+normalizeDir =
+  clean . FilePath.addTrailingPathSeparator . FilePath.normalise
+  where clean "./" = ""
+        clean ('/':'/':xs) = clean ('/':xs)
+        clean x = x
+
+-- | Internal use for normalizing a fileectory.
+normalizeFile :: FilePath -> FilePath
+normalizeFile =
+  clean . FilePath.normalise
+  where clean "./" = ""
+        clean ('/':'/':xs) = clean ('/':xs)
+        clean x = x
diff --git a/src/Path/Internal.hs b/src/Path/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Path/Internal.hs
@@ -0,0 +1,49 @@
+{-# LANGUAGE StandaloneDeriving #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
+-- | Internal types and functions.
+
+module Path.Internal
+  (Path(..))
+  where
+
+import Data.Data
+import GHC.Generics
+
+-- | Path of some base and type.
+--
+-- Internally is a string. The string can be of two formats only:
+--
+-- 1. File format: @file.txt@, @foo\/bar.txt@, @\/foo\/bar.txt@
+-- 2. Directory format: @foo\/@, @\/foo\/bar\/@
+--
+-- All directories end in a trailing separator. There are no duplicate
+-- path separators @\/\/@, no @..@, no @.\/@, no @~\/@, etc.
+newtype Path b t = Path FilePath
+  deriving (Typeable,Generic)
+
+-- | String equality.
+--
+-- The following property holds:
+--
+-- @show x == show y ≡ x == y@
+instance Eq (Path b t) where
+  (==) (Path x) (Path y) = x == y
+
+-- | String ordering.
+--
+-- The following property holds:
+--
+-- @show x \`compare\` show y ≡ x \`compare\` y@
+instance Ord (Path b t) where
+  compare (Path x) (Path y) = compare x y
+
+-- | Same as 'Path.toFilePath'.
+--
+-- The following property holds:
+--
+-- @x == y ≡ show x == show y@
+instance Show (Path b t) where
+  show (Path x) = show x
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,190 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+-- | Test suite.
+
+module Main where
+
+import Data.Monoid
+import Path
+import Path.Internal
+import Test.Hspec
+
+-- | Test suite entry point, returns exit failure if any test fails.
+main :: IO ()
+main = hspec spec
+
+-- | Test suite.
+spec :: Spec
+spec =
+  do describe "Parsing: Path Abs Dir" parseAbsDirSpec
+     describe "Parsing: Path Rel Dir" parseRelDirSpec
+     describe "Parsing: Path Abs File" parseAbsFileSpec
+     describe "Parsing: Path Rel File" parseRelFileSpec
+     describe "Operations: (</>)" operationAppend
+     describe "Operations: stripDir" operationStripDir
+     describe "Operations: isParentOf" operationIsParentOf
+     describe "Operations: parentAbs" operationParentAbs
+     describe "Operations: filename" operationFilename
+
+-- | The 'filename' operation.
+operationFilename :: Spec
+operationFilename =
+  do it "filename ($(mkAbsDir parent) </> filename $(mkRelFile filename)) == $(mkRelFile filename)"
+        (filename ($(mkAbsDir "/home/chris/") </>
+                   filename $(mkRelFile "bar.txt")) ==
+         $(mkRelFile "bar.txt"))
+     it "filename ($(mkRelDir parent) </> filename $(mkRelFile filename)) == $(mkRelFile filename)"
+        (filename ($(mkRelDir "home/chris/") </>
+                   filename $(mkRelFile "bar.txt")) ==
+         $(mkRelFile "bar.txt"))
+
+-- | The 'parentAbs' operation.
+operationParentAbs :: Spec
+operationParentAbs =
+  do it "parentAbs (parent </> child) == parent"
+        (parentAbs ($(mkAbsDir "/foo") </>
+                    $(mkRelDir "bar")) ==
+         $(mkAbsDir "/foo"))
+     it "parentAbs \"\" == \"\""
+        (parentAbs $(mkAbsDir "/") ==
+         $(mkAbsDir "/"))
+     it "parentAbs (parentAbs \"\") == \"\""
+        (parentAbs (parentAbs $(mkAbsDir "/")) ==
+         $(mkAbsDir "/"))
+
+-- | The 'isParentOf' operation.
+operationIsParentOf :: Spec
+operationIsParentOf =
+  do it "isParentOf parent (parent </> child)"
+        (isParentOf
+           $(mkAbsDir "///bar/")
+           ($(mkAbsDir "///bar/") </>
+            $(mkRelFile "bar/foo.txt")))
+     it "isParentOf parent (parent </> child)"
+        (isParentOf
+           $(mkRelDir "bar/")
+           ($(mkRelDir "bar/") </>
+            $(mkRelFile "bob/foo.txt")))
+
+-- | The 'stripDir' operation.
+operationStripDir :: Spec
+operationStripDir =
+  do it "stripDir parent (parent </> child) = child"
+        (stripDir $(mkAbsDir "///bar/")
+                  ($(mkAbsDir "///bar/") </>
+                   $(mkRelFile "bar/foo.txt")) ==
+         Just $(mkRelFile "bar/foo.txt"))
+     it "stripDir parent (parent </> child) = child"
+        (stripDir $(mkRelDir "bar/")
+                  ($(mkRelDir "bar/") </>
+                   $(mkRelFile "bob/foo.txt")) ==
+         Just $(mkRelFile "bob/foo.txt"))
+
+-- | The '</>' operation.
+operationAppend :: Spec
+operationAppend =
+  do it "AbsDir + RelDir = AbsDir"
+        ($(mkAbsDir "/home/") </>
+         $(mkRelDir "chris") ==
+         $(mkAbsDir "/home/chris/"))
+     it "AbsDir + RelFile = AbsFile"
+        ($(mkAbsDir "/home/") </>
+         $(mkRelFile "chris/test.txt") ==
+         $(mkAbsFile "/home/chris/test.txt"))
+     it "RelDir + RelDir = RelDir"
+        ($(mkRelDir "home/") </>
+         $(mkRelDir "chris") ==
+         $(mkRelDir "home/chris"))
+     it "RelDir + RelFile = RelFile"
+        ($(mkRelDir "home/") </>
+         $(mkRelFile "chris/test.txt") ==
+         $(mkRelFile "home/chris/test.txt"))
+
+-- | Tests for the tokenizer.
+parseAbsDirSpec :: Spec
+parseAbsDirSpec =
+  do failing ""
+     failing "./"
+     failing "~/"
+     failing "foo.txt"
+     succeeding "/" (Path "/")
+     succeeding "//" (Path "/")
+     succeeding "///foo//bar//mu/" (Path "/foo/bar/mu/")
+     succeeding "///foo//bar////mu" (Path "/foo/bar/mu/")
+     succeeding "///foo//bar/.//mu" (Path "/foo/bar/mu/")
+  where failing x = parserTest parseAbsDir x Nothing
+        succeeding x with = parserTest parseAbsDir x (Just with)
+
+-- | Tests for the tokenizer.
+parseRelDirSpec :: Spec
+parseRelDirSpec =
+  do failing ""
+     failing "/"
+     failing "//"
+     failing "~/"
+     failing "/"
+     failing "./"
+     failing "//"
+     failing "///foo//bar//mu/"
+     failing "///foo//bar////mu"
+     failing "///foo//bar/.//mu"
+     succeeding "foo.bak" (Path "foo.bak/")
+     succeeding "./foo" (Path "foo/")
+     succeeding "foo//bar//mu//" (Path "foo/bar/mu/")
+     succeeding "foo//bar////mu" (Path "foo/bar/mu/")
+     succeeding "foo//bar/.//mu" (Path "foo/bar/mu/")
+  where failing x = parserTest parseRelDir x Nothing
+        succeeding x with = parserTest parseRelDir x (Just with)
+
+-- | Tests for the tokenizer.
+parseAbsFileSpec :: Spec
+parseAbsFileSpec =
+  do failing ""
+     failing "./"
+     failing "~/"
+     failing "./foo.txt"
+     failing "/"
+     failing "//"
+     failing "///foo//bar//mu/"
+     succeeding "/foo.txt" (Path "/foo.txt")
+     succeeding "///foo//bar////mu.txt" (Path "/foo/bar/mu.txt")
+     succeeding "///foo//bar/.//mu.txt" (Path "/foo/bar/mu.txt")
+  where failing x = parserTest parseAbsFile x Nothing
+        succeeding x with = parserTest parseAbsFile x (Just with)
+
+-- | Tests for the tokenizer.
+parseRelFileSpec :: Spec
+parseRelFileSpec =
+  do failing ""
+     failing "/"
+     failing "//"
+     failing "~/"
+     failing "/"
+     failing "./"
+     failing "//"
+     failing "///foo//bar//mu/"
+     failing "///foo//bar////mu"
+     failing "///foo//bar/.//mu"
+     succeeding "foo.txt" (Path "foo.txt")
+     succeeding "./foo.txt" (Path "foo.txt")
+     succeeding "foo//bar//mu.txt" (Path "foo/bar/mu.txt")
+     succeeding "foo//bar////mu.txt" (Path "foo/bar/mu.txt")
+     succeeding "foo//bar/.//mu.txt" (Path "foo/bar/mu.txt")
+  where failing x = parserTest parseRelFile x Nothing
+        succeeding x with = parserTest parseRelFile x (Just with)
+
+-- | Parser test.
+parserTest :: (Show a1,Show a,Eq a1)
+           => (a -> Maybe a1) -> a -> Maybe a1 -> SpecWith ()
+parserTest parser input expected =
+  it ((case expected of
+         Nothing -> "Failing: "
+         Just{} -> "Succeeding: ") <>
+      "Parsing " <>
+      show input <>
+      " " <>
+      case expected of
+        Nothing -> "should fail."
+        Just x -> "should succeed with: " <> show x)
+     (actual == expected)
+  where actual = parser input
