diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Richard Marko (c) 2020
+
+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 Author name here 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,7 @@
+nix-narinfo
+===========
+
+Parse and build `.narinfo` files.
+
+Loosely based on [cachix-api](https://github.com/cachix/cachix/tree/master/cachix-api) `NarInfo` type.
+Inspired by [nix-derivation](https://github.com/Gabriel439/Haskell-Nix-Derivation-Library/) library.
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/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,15 @@
+module Main where
+
+import Nix.NarInfo
+
+import Data.Attoparsec.Text
+import Data.Text.IO
+import Data.Text.Lazy.Builder
+import Data.Text.Lazy.IO
+
+main = do
+  txt <- Data.Text.IO.readFile "./test/samples/0"
+  case Data.Attoparsec.Text.parseOnly parseNarInfo txt of
+    Left e -> error e
+    Right ni -> Data.Text.Lazy.IO.putStr $
+      Data.Text.Lazy.Builder.toLazyText $ buildNarInfo ni
diff --git a/nix-narinfo.cabal b/nix-narinfo.cabal
new file mode 100644
--- /dev/null
+++ b/nix-narinfo.cabal
@@ -0,0 +1,68 @@
+name:                nix-narinfo
+version:             0.1.0.0
+synopsis:            Parse and render .narinfo files
+description:         Support for parsing and rendering Nix .narinfo files
+homepage:            https://github.com/sorki/nix-narinfo
+license:             BSD3
+license-file:        LICENSE
+author:              Richard Marko
+maintainer:          srk@48.io
+copyright:           2020 Richard Marko
+category:            Nix
+build-type:          Simple
+cabal-version:       >=1.10
+
+extra-source-files:
+    README.md
+
+library
+  hs-source-dirs:      src
+  exposed-modules:     Nix.NarInfo
+  other-modules:       Nix.NarInfo.Builder
+                     , Nix.NarInfo.Parser
+                     , Nix.NarInfo.Types
+  build-depends:       base >= 4.7 && < 5
+                     , containers
+                     , text
+                     , attoparsec
+  default-language:    Haskell2010
+
+executable pretty-narinfo
+  hs-source-dirs:      app
+  main-is:             Main.hs
+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
+  build-depends:       base
+                     , attoparsec
+                     , text
+                     , nix-narinfo
+  default-language:    Haskell2010
+
+
+test-suite nix-narinfo-samples
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             Spec.hs
+  other-modules:       ParseSpec
+                       SpecHelper
+  build-depends:       base >= 4.7 && < 5
+                     , attoparsec
+                     , text
+                     , nix-narinfo
+                     , hspec
+  default-language:    Haskell2010
+
+test-suite nix-narinfo-property
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             Property.hs
+  build-depends:       base >= 4.7 && < 5
+                     , attoparsec
+                     , filepath
+                     , text
+                     , nix-narinfo
+                     , QuickCheck
+  default-language:    Haskell2010
+
+source-repository head
+  type:     git
+  location: https://github.com/sorki/nix-narinfo
diff --git a/src/Nix/NarInfo.hs b/src/Nix/NarInfo.hs
new file mode 100644
--- /dev/null
+++ b/src/Nix/NarInfo.hs
@@ -0,0 +1,10 @@
+module Nix.NarInfo (
+    module Nix.NarInfo.Builder
+  , module Nix.NarInfo.Parser
+  , module Nix.NarInfo.Types
+  ) where
+
+import Nix.NarInfo.Builder
+import Nix.NarInfo.Parser
+import Nix.NarInfo.Types
+
diff --git a/src/Nix/NarInfo/Builder.hs b/src/Nix/NarInfo/Builder.hs
new file mode 100644
--- /dev/null
+++ b/src/Nix/NarInfo/Builder.hs
@@ -0,0 +1,58 @@
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Nix.NarInfo.Builder
+    ( -- * Builder
+      buildNarInfo
+    , buildNarInfoWith
+    ) where
+
+import Data.Set (Set)
+import Data.Text (Text)
+import Data.Text.Lazy.Builder (Builder)
+import Nix.NarInfo.Types
+
+import qualified Control.Applicative
+import qualified Data.Char
+import qualified Data.Set
+import qualified Data.List
+import qualified Data.Text
+import qualified Data.Text.Lazy.Builder
+
+buildNarInfo :: (NarInfo FilePath Text Text) -> Builder
+buildNarInfo = buildNarInfoWith
+  (\_needPrefix fp -> Data.Text.Lazy.Builder.fromString fp)
+  Data.Text.Lazy.Builder.fromText
+  Data.Text.Lazy.Builder.fromText
+
+buildNarInfoWith :: (Ord fp)
+                 => (Bool -> fp -> Builder)
+                 -> (txt -> Builder)
+                 -> (hash -> Builder)
+                 -> NarInfo fp txt hash
+                 -> Builder
+buildNarInfoWith filepath string hash (NarInfo{..}) =
+     keyPath "StorePath"   storePath
+  <> key     "URL"         url
+  <> key     "Compression" compression
+  <> keyHash "FileHash"    fileHash
+  <> keyNum  "FileSize"    fileSize
+  <> keyHash "NarHash"     narHash
+  <> keyNum  "NarSize"     narSize
+
+  <> key'    "References"  (mconcat
+      $ Data.List.intersperse " "
+        $ map (filepath False)
+          $ Data.List.sort $ Data.Set.toList references)
+
+  <> optKey  "Deriver"     deriver
+  <> optKey  "System"      system
+  <> optKey  "Sig"         sig
+  <> optKey  "Ca"          ca
+  where
+    key' k v    = k <> ": " <> v <> "\n"
+    key k v     = key' k (string v)
+    keyNum k v  = key' k (Data.Text.Lazy.Builder.fromString . show $ v)
+    keyPath k v = key' k (filepath True v)
+    keyHash k v = key' k (hash v)
+    optKey k    = maybe mempty (key k)
diff --git a/src/Nix/NarInfo/Parser.hs b/src/Nix/NarInfo/Parser.hs
new file mode 100644
--- /dev/null
+++ b/src/Nix/NarInfo/Parser.hs
@@ -0,0 +1,63 @@
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Nix.NarInfo.Parser
+  ( -- * Parser
+    parseNarInfo
+  , parseNarInfoWith
+  ) where
+
+import Data.Set (Set)
+import Data.Text (Text)
+import Data.Attoparsec.Text (Parser)
+import Nix.NarInfo.Types
+
+import qualified Control.Applicative
+import qualified Data.Char
+import qualified Data.Set
+import qualified Data.Text
+import qualified Data.Attoparsec.Text
+
+parseNarInfo :: Parser (NarInfo FilePath Text Text)
+parseNarInfo = parseNarInfoWith pathParse textParse hashParse
+  where
+    textParse = Data.Attoparsec.Text.takeWhile (not . Data.Char.isSpace)
+    pathParse _hasPrefix = Data.Text.unpack <$> textParse
+    hashParse = textParse
+
+parseNarInfoWith :: (Ord fp)
+                 => (Bool -> Parser fp) -- True when path prefix is present
+                 -> Parser txt
+                 -> Parser hash
+                 -> Parser (NarInfo fp txt hash)
+parseNarInfoWith pathParser textParser hashParser = do
+  storePath   <- keyPath "StorePath"
+  url         <- key     "URL"
+  compression <- key     "Compression"
+  fileHash    <- keyHash "FileHash"
+  fileSize    <- keyNum  "FileSize"
+  narHash     <- keyHash "NarHash"
+  narSize     <- keyNum  "NarSize"
+
+  references  <- Data.Set.fromList <$> (parseKey "References" $
+    (pathParser False) `Data.Attoparsec.Text.sepBy` Data.Attoparsec.Text.char ' ')
+
+  deriver     <- optKey "Deriver"
+  system      <- optKey "System"
+  sig         <- optKey "Sig"
+  ca          <- optKey "Ca"
+
+  return $ NarInfo {..}
+  where
+    parseKey key parser = do
+      Data.Attoparsec.Text.string key
+      Data.Attoparsec.Text.string ": "
+      out <- parser
+      Data.Attoparsec.Text.char '\n'
+      return out
+
+    key = flip parseKey textParser
+    optKey = Control.Applicative.optional . key
+    keyNum x = parseKey x Data.Attoparsec.Text.decimal
+    keyPath x = parseKey x (pathParser True)
+    keyHash x = parseKey x hashParser
diff --git a/src/Nix/NarInfo/Types.hs b/src/Nix/NarInfo/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Nix/NarInfo/Types.hs
@@ -0,0 +1,63 @@
+{-# LANGUAGE DeriveGeneric     #-}
+{-# LANGUAGE DeriveAnyClass    #-}
+
+
+module Nix.NarInfo.Types
+    ( -- * Types
+      NarInfo(..)
+    , SimpleNarInfo
+    ) where
+
+import Data.Set (Set)
+import Data.Text (Text)
+import GHC.Generics
+
+-- NarInfo URL includes storePath hash and .narinfo suffix
+-- Note: storePath is with prefix but references are shortRefs (without /nix/store prefix)
+--
+-- Both `parseNarInfoWith` and `buildNarInfoWith` need
+-- a path parser/printer which takes an argument
+-- whether the path is prefixed or not.
+--
+data NarInfo fp txt hash = NarInfo
+  { -- | Absolute path of the derivation in nix store.
+    storePath   :: fp
+  , -- | Relative url (to current domain) to download nar file.
+    url         :: txt
+  , -- | Name of the compression algorithm, eg. xz.
+    compression :: txt
+  , -- | Hash of the compressed nar file.
+    -- NOTE: to compute use "nix-hash --type sha256 --flat"
+    -- (srk) this isn't fixed to sha256 but a prefix indicates the type e.g.: sha256:1a6lzf...
+    -- default is sha256 thought
+    fileHash    :: hash
+  , -- | File size of compressed nar file.
+    -- NOTE: du -b
+    fileSize    :: Integer
+  , -- | Hash of the decompressed nar file.
+    -- NOTE: to compute use "nix-hash --type sha256 --flat --base32"
+    -- (srk) this isn't fixed to sha256 but a prefix indicates the type e.g.: sha256:1a6lzf...
+    -- default is sha256 thought
+    narHash     :: hash
+  , -- | File size of decompressed nar file.
+    -- NOTE: du -b
+    narSize     :: Integer
+  , -- | Immediate dependencies of the storePath.
+    -- NOTE: nix-store -q --references
+    references  :: Set fp
+  , -- | Relative store path (to nix store root) of the deriver.
+    -- NOTE: nix-store -q --deriver
+    deriver     :: Maybe txt
+  , -- | System
+    system      :: Maybe txt
+  , -- | Signature of fields: storePath, narHash, narSize, refs.
+    sig         :: Maybe txt
+  , -- | Content-addressed
+    -- Store path is computed from a cryptographic hash
+    -- of the contents of the path, plus some other bits of data like
+    -- the "name" part of the path.
+    ca          :: Maybe txt
+  }
+  deriving (Eq, Generic, Show)
+
+type SimpleNarInfo = NarInfo FilePath Text Text
diff --git a/test/ParseSpec.hs b/test/ParseSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/ParseSpec.hs
@@ -0,0 +1,28 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TemplateHaskell   #-}
+
+module ParseSpec where
+
+import Data.Attoparsec.Text
+import Data.Text.IO
+import Data.Text.Lazy
+import Data.Text.Lazy.Builder
+import Data.Text.Lazy.IO
+
+import SpecHelper
+
+roundTrip fname = do
+  txt <- Data.Text.IO.readFile $ "./test/samples/" ++ fname
+  case Data.Attoparsec.Text.parseOnly parseNarInfo txt of
+    Left e -> error e
+    Right ni -> do
+      let built = Data.Text.Lazy.Builder.toLazyText $ buildNarInfo ni
+      (Data.Text.Lazy.toStrict built) `shouldBe` txt
+
+spec :: Spec
+spec = do
+  it "roundtrips samples" $ do
+    mapM_ (roundTrip . show) [0]
+
+main :: IO ()
+main = hspec spec
diff --git a/test/Property.hs b/test/Property.hs
new file mode 100644
--- /dev/null
+++ b/test/Property.hs
@@ -0,0 +1,53 @@
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE TypeSynonymInstances #-}
+{-# LANGUAGE FlexibleInstances #-}
+
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+module Main where
+
+import Data.Text (Text)
+import System.FilePath
+import Nix.NarInfo (NarInfo(..))
+import Test.QuickCheck (Arbitrary(..))
+
+import qualified Data.Attoparsec.Text.Lazy
+import qualified Data.Text
+import qualified Data.Text.Lazy.Builder
+import qualified Nix.NarInfo
+import qualified Test.QuickCheck
+
+instance Arbitrary Text where
+    arbitrary = fmap Data.Text.pack arbitrary
+
+instance Arbitrary (NarInfo FilePath Text Text) where
+    arbitrary = do
+        storePath   <- arbitrary
+        url         <- arbitrary
+        compression <- arbitrary
+        fileHash    <- arbitrary
+        fileSize    <- arbitrary
+        narHash     <- arbitrary
+        narSize     <- arbitrary
+        references  <- arbitrary
+        deriver     <- arbitrary
+        system      <- arbitrary
+        sig         <- arbitrary
+        ca          <- arbitrary
+        return (NarInfo {..})
+
+property :: NarInfo FilePath Text Text -> Bool
+property narinfo0 = eitherRes == Right narinfo0
+  where
+    builder = Nix.NarInfo.buildNarInfo narinfo0
+
+    text = Data.Text.Lazy.Builder.toLazyText builder
+
+    result =
+        Data.Attoparsec.Text.Lazy.parse Nix.NarInfo.parseNarInfo text
+
+    eitherRes =
+        Data.Attoparsec.Text.Lazy.eitherResult result
+
+main :: IO ()
+main = Test.QuickCheck.quickCheck property
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
diff --git a/test/SpecHelper.hs b/test/SpecHelper.hs
new file mode 100644
--- /dev/null
+++ b/test/SpecHelper.hs
@@ -0,0 +1,7 @@
+module SpecHelper
+    ( module Test.Hspec
+    , module Nix.NarInfo
+    ) where
+
+import Test.Hspec
+import Nix.NarInfo
