diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,3 @@
+# Changelog for watch-fs-hs
+
+## Unreleased changes
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Author name here (c) 2021
+
+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,57 @@
+# Unix Recursive
+
+![build](https://github.com/turboMaCk/unix-recursive/workflows/Test/badge.svg?branch=main&event=push)
+
+Blazingly fast functions for recursive file system operations.
+Utilizing lazy IO for constant space & computation efficiant bindigns to Posix [`dirstream.h`](https://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/unix/dirstream.h;h=8303f07fab6f6efaa39e51411ef924e712d995e0;hb=fa39685d5c7df2502213418bead44e9543a9b9ec) api.
+
+## Comparision with other libraries:
+
+Tests were performed on relatively modern consumer grade HW with relatively modern consumer grade M.2 SSD drive.
+All done on binaries compiled with optimization that are listing the same directory
+with 1,705,184 items.
+
+### [this lib] Unix Recursive `RawFilePath`/ByteString
+
+![](docs/unix-recursive-bytestring.png)
+
+- [source](bin/unix-recursive-bytestring.hs)
+
+### [this lib] Unix Recursive `FilePath`/String
+
+![](docs/unix-recursive-string.png)
+
+- [source](bin/unix-recursive-string.hs)
+
+### [alternative] Dir Traverse (`FilePath`/String is the only option)
+
+![](docs/dir-traverse.png)
+
+- [source](bin/dir-traverse.hs)
+- [lib](https://hackage.haskell.org/package/dir-traverse)
+
+### [alternative] Dirstream (`Filesystem.Path` is the only option)
+
+![](docs/dirstream.png)
+
+- [source](bin/dirstream.hs)
+- [lib](https://hackage.haskell.org/package/dirstream)
+
+## Testing performance
+
+Cabal flag `bin` is being used for building the example binaries that can be used for measurement.
+
+```
+make bin
+```
+
+or to build and run one of the binaries:
+
+```
+$ stack build --flag unix-recursive:bin --exec "unix-recursive-bytestring-bin ${path-to-directory}"
+```
+
+## Hacking
+
+This project uses the "Perfect Haskell Preprocessor" (PHP) for generating haskell source from meta module hs.
+See [`gen`](gen) directory and [`MakeFile`](MakeFile) before you start hackking on stuff.
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/System/Posix/Recursive.hs b/System/Posix/Recursive.hs
new file mode 100644
--- /dev/null
+++ b/System/Posix/Recursive.hs
@@ -0,0 +1,274 @@
+{- |
+ Module      : System.Posix.Recursive-- Copyright   : (c) Marek Fajkus
+ License     : BSD3
+
+ Maintainer  : marek.faj@gmail.com
+
+ All modules profided by @unix-recursive@ expose similar API.
+ Make sure you're using the module wich best fits your needs based on:
+   - Working  with 'RawFilePath' (faster and more packed) or 'FilePath' (slower but easier to work with safely)
+   - Exception free (Default) or @Unsafe@ variants of functions
+
+ = Usage
+
+ This module is designed to be imported as @qualified@:
+
+ > import qualified System.Posix.Recursive as PosixRecursive
+
+ __Results__
+
+ All functions return will return root path (the one they accept in argument) as a first item in the list:
+
+ > head <$> PosixRecursive.list "System"
+ > >>> "System"
+
+ Other than that, this library __provides no guarantees about the order in which files appear in the resulting list__
+ to make it possible to change the underlaying strategy in future versions.
+
+ __Laziness__
+
+ All IO operations are __guaranteed to be lazy and have constanct space characteristic__.
+ Only the IO required by lazy computation will be performed so for instance running code like:
+
+ > take 20 <$> PosixRecursive.listDirectories "/"
+
+ Will perform only minimal ammount of IO needed to collect 20 directories on a root partition
+-}
+module System.Posix.Recursive (
+    -- * Basic Listing
+    -- $basic_listing
+    list,
+    followList,
+    listMatching,
+    followListMatching,
+
+    -- * File Type Based Listing
+    -- $type_based
+    listAccessible,
+    listDirectories,
+    listRegularFiles,
+    listSymbolicLinks,
+
+    -- * Custom Listing
+    -- $custom
+    Conf (..),
+    defConf,
+    listCustom,
+) where
+
+import Control.Exception (bracket, handle)
+import Data.Foldable (fold)
+import System.IO.Unsafe (unsafeInterleaveIO)
+import System.Posix.Files (FileStatus)
+
+import qualified System.Posix.Directory as Posix
+import qualified System.Posix.Files as Posix
+
+
+-- Helpers
+
+foldMapA :: (Monoid b, Traversable t, Applicative f) => (a -> f b) -> t a -> f b
+foldMapA = (fmap fold .) . traverse
+
+
+{-# INLINE listDir #-}
+listDir :: (FilePath -> Bool) -> FilePath -> IO [FilePath]
+listDir predicate path =
+    bracket
+        (Posix.openDirStream path)
+        Posix.closeDirStream
+        (go [])
+  where
+    go :: [FilePath] -> Posix.DirStream -> IO [FilePath]
+    go acc dirp = do
+        e <- Posix.readDirStream dirp
+        if null e
+            then pure acc
+            else
+                if e /= "." && e /= ".."
+                    then
+                        let fullPath = path <> "/" <> e
+                         in if predicate fullPath
+                                then go (fullPath : acc) dirp
+                                else go acc dirp
+                    else go acc dirp
+{- $basic_listing
+ Functions for listing contents of directory recursively.
+ These functions list all the content they encounter while traversing
+ the file system tree including directories, files, symlinks, broken symlinks.
+
+  __Directories (and files) which process can't open due to permissions are listed but not recursed into.__
+
+ Functions from this section is gurantee to always return the root path as a first element even
+ if this path does not exist.
+
+ > PosixRecursive.list "i-dont-exist"
+ > >>> ["i-dont-exist"]
+
+ In these cases the root path is considered the same way as symlink
+ to non existing location.
+-}
+
+
+{-# INLINE listAll' #-}
+listAll' :: Bool -> (FilePath -> Bool) -> FilePath -> IO [FilePath]
+listAll' followSymlinks predicate path =
+    handle (\(_ :: IOError) -> pure []) $ do
+        file <- getFileStatus path
+        if Posix.isDirectory file
+            then do
+                content <- listDir predicate path
+
+                next <- unsafeInterleaveIO $ foldMapA (listAll' followSymlinks predicate) content
+                pure $ content ++ next
+            else pure []
+  where
+    {-# INLINE getFileStatus #-}
+    getFileStatus
+        | followSymlinks = Posix.getFileStatus
+        | otherwise = Posix.getSymbolicLinkStatus
+
+
+{-# INLINE listAll'' #-}
+listAll'' :: Bool -> (FilePath -> Bool) -> FilePath -> IO [FilePath]
+listAll'' followSymlinks predicate path =
+    (path :) <$> listAll' followSymlinks predicate path
+
+
+-- | Like 'list' but uses provided function to test in which 'FilePath' to recurse into.
+listMatching :: (FilePath -> Bool) -> FilePath -> IO [FilePath]
+listMatching =
+    listAll'' False
+
+
+-- | Like 'followList' but uses provided function to test in which 'FilePath' to recurse into.
+followListMatching :: (FilePath -> Bool) -> FilePath -> IO [FilePath]
+followListMatching =
+    listAll'' True
+
+
+{- | List all files, directories & symlinks recursively.
+ Symlinks are not followed. See 'followList'.
+-}
+list :: FilePath -> IO [FilePath]
+list =
+    listAll'' False (const True)
+
+
+{- | List all files, directories & symlinks recursively.
+ Unlike 'list', symlinks are followed recursively as well.
+-}
+followList :: FilePath -> IO [FilePath]
+followList =
+    listAll'' True (const True)
+
+
+{- $custom
+ All /File Type Based Listing/ functions are based on top of this interface.
+ This part of API exposes exposes access for writing custom filtering functions.
+
+ All paths are tested for filter functions so unreadble files won't appear in the result list:
+
+ > PosixRecursive.listCustom PosixRecursive.defConf "i-dont-exist"
+ > >>> []
+
+ It's not possible to turn of this behaviour because this functions must get the 'FileStatus'
+ type which requires reading each entry.
+-}
+
+
+-- | Configuration arguments for 'listCustom'.
+data Conf = Conf
+    { -- | Filter paths algorithm should recurse into
+      filterPath :: !(FilePath -> Bool)
+    , -- | Test if file should be included in returned List
+      includeFile :: !(FileStatus -> FilePath -> IO Bool)
+    , -- | Follow symbolic links
+      followSymlinks :: !Bool
+    }
+
+
+{- | Default 'Conf'iguration.
+
+> listCustom defConf == listAccessible
+
+* Recurses into all Paths
+* Includes all file types
+* Does __not__ follow symlinks
+-}
+defConf :: Conf
+defConf =
+    Conf
+        { filterPath = const True
+        , includeFile = \_ _ -> pure True
+        , followSymlinks = False
+        }
+
+
+{-# INLINE listAccessible' #-}
+listAccessible' :: Conf -> FilePath -> IO [FilePath]
+listAccessible' Conf{..} path =
+    handle (\(_ :: IOError) -> pure []) $ do
+        file <- getFileStatus path
+        next <-
+            if Posix.isDirectory file
+                then do
+                    content <- listDir filterPath path
+                    unsafeInterleaveIO $ foldMapA (listAccessible' Conf{..}) content
+                else pure []
+
+        include <- includeFile file path
+        if include
+            then pure $ path : next
+            else pure next
+  where
+    {-# INLINE getFileStatus #-}
+    getFileStatus
+        | followSymlinks = Posix.getFileStatus
+        | otherwise = Posix.getSymbolicLinkStatus
+
+
+-- | Recursively list files using custom filters.
+listCustom :: Conf -> FilePath -> IO [FilePath]
+listCustom =
+    listAccessible'
+
+
+{- | Like 'list' but automatically filters out inacessible files like broken symlins
+or unreadable files and directories.
+-}
+listAccessible :: FilePath -> IO [FilePath]
+listAccessible =
+    listAccessible' defConf
+
+
+{- $type_based
+ Functions for listing specific file type. Reading the file type requires
+ ability to read the file.
+
+  __These function do not return broken symlinks and inacessible files__
+
+ Include test is applied even for the root entry (path past in as an argument).
+ This means that non existing paths are filtered.
+
+ > PosixRecursive.listDirectories "i-dont-exist"
+ > >>> []
+-}
+
+
+-- | List sub directories of given directory.
+listDirectories :: FilePath -> IO [FilePath]
+listDirectories =
+    listAccessible' defConf{includeFile = \file _ -> pure $ Posix.isDirectory file}
+
+
+-- | Lists only files (while recursing into directories still).
+listRegularFiles :: FilePath -> IO [FilePath]
+listRegularFiles =
+    listAccessible' defConf{includeFile = \file _ -> pure $ Posix.isRegularFile file}
+
+
+-- | Lists only symbolic links (while recursing into directories still).
+listSymbolicLinks :: FilePath -> IO [FilePath]
+listSymbolicLinks =
+    listAccessible' defConf{includeFile = \file _ -> pure $ Posix.isSymbolicLink file}
diff --git a/System/Posix/Recursive/ByteString.hs b/System/Posix/Recursive/ByteString.hs
new file mode 100644
--- /dev/null
+++ b/System/Posix/Recursive/ByteString.hs
@@ -0,0 +1,277 @@
+{- |
+ Module      : System.Posix.Recursive.ByteString-- Copyright   : (c) Marek Fajkus
+ License     : BSD3
+
+ Maintainer  : marek.faj@gmail.com
+
+ All modules profided by @unix-recursive@ expose similar API.
+ Make sure you're using the module wich best fits your needs based on:
+   - Working  with 'RawFilePath' (faster and more packed) or 'FilePath' (slower but easier to work with safely)
+   - Exception free (Default) or @Unsafe@ variants of functions
+
+ = Usage
+
+ This module is designed to be imported as @qualified@:
+
+ > import qualified System.Posix.Recursive.ByteString as PosixRecursive
+
+ __Results__
+
+ All functions return will return root path (the one they accept in argument) as a first item in the list:
+
+ > head <$> PosixRecursive.list "System"
+ > >>> "System"
+
+ Other than that, this library __provides no guarantees about the order in which files appear in the resulting list__
+ to make it possible to change the underlaying strategy in future versions.
+
+ __Laziness__
+
+ All IO operations are __guaranteed to be lazy and have constanct space characteristic__.
+ Only the IO required by lazy computation will be performed so for instance running code like:
+
+ > take 20 <$> PosixRecursive.listDirectories "/"
+
+ Will perform only minimal ammount of IO needed to collect 20 directories on a root partition
+-}
+module System.Posix.Recursive.ByteString (
+    -- * Basic Listing
+    -- $basic_listing
+    list,
+    followList,
+    listMatching,
+    followListMatching,
+
+    -- * File Type Based Listing
+    -- $type_based
+    listAccessible,
+    listDirectories,
+    listRegularFiles,
+    listSymbolicLinks,
+
+    -- * Custom Listing
+    -- $custom
+    Conf (..),
+    defConf,
+    listCustom,
+) where
+
+import Control.Exception (bracket, handle)
+import Data.Foldable (fold)
+import System.IO.Unsafe (unsafeInterleaveIO)
+import System.Posix.Files (FileStatus)
+
+import qualified Data.ByteString as BS
+import System.Posix.ByteString.FilePath (RawFilePath)
+
+import qualified System.Posix.Directory.ByteString as Posix
+import qualified System.Posix.Files.ByteString as Posix
+
+
+-- Helpers
+
+foldMapA :: (Monoid b, Traversable t, Applicative f) => (a -> f b) -> t a -> f b
+foldMapA = (fmap fold .) . traverse
+
+
+{-# INLINE listDir #-}
+listDir :: (RawFilePath -> Bool) -> RawFilePath -> IO [RawFilePath]
+listDir predicate path =
+    bracket
+        (Posix.openDirStream path)
+        Posix.closeDirStream
+        (go [])
+  where
+    go :: [RawFilePath] -> Posix.DirStream -> IO [RawFilePath]
+    go acc dirp = do
+        e <- Posix.readDirStream dirp
+        if BS.null e
+            then pure acc
+            else
+                if e /= "." && e /= ".."
+                    then
+                        let fullPath = path <> "/" <> e
+                         in if predicate fullPath
+                                then go (fullPath : acc) dirp
+                                else go acc dirp
+                    else go acc dirp
+{- $basic_listing
+ Functions for listing contents of directory recursively.
+ These functions list all the content they encounter while traversing
+ the file system tree including directories, files, symlinks, broken symlinks.
+
+  __Directories (and files) which process can't open due to permissions are listed but not recursed into.__
+
+ Functions from this section is gurantee to always return the root path as a first element even
+ if this path does not exist.
+
+ > PosixRecursive.list "i-dont-exist"
+ > >>> ["i-dont-exist"]
+
+ In these cases the root path is considered the same way as symlink
+ to non existing location.
+-}
+
+
+{-# INLINE listAll' #-}
+listAll' :: Bool -> (RawFilePath -> Bool) -> RawFilePath -> IO [RawFilePath]
+listAll' followSymlinks predicate path =
+    handle (\(_ :: IOError) -> pure []) $ do
+        file <- getFileStatus path
+        if Posix.isDirectory file
+            then do
+                content <- listDir predicate path
+
+                next <- unsafeInterleaveIO $ foldMapA (listAll' followSymlinks predicate) content
+                pure $ content ++ next
+            else pure []
+  where
+    {-# INLINE getFileStatus #-}
+    getFileStatus
+        | followSymlinks = Posix.getFileStatus
+        | otherwise = Posix.getSymbolicLinkStatus
+
+
+{-# INLINE listAll'' #-}
+listAll'' :: Bool -> (RawFilePath -> Bool) -> RawFilePath -> IO [RawFilePath]
+listAll'' followSymlinks predicate path =
+    (path :) <$> listAll' followSymlinks predicate path
+
+
+-- | Like 'list' but uses provided function to test in which 'RawFilePath' to recurse into.
+listMatching :: (RawFilePath -> Bool) -> RawFilePath -> IO [RawFilePath]
+listMatching =
+    listAll'' False
+
+
+-- | Like 'followList' but uses provided function to test in which 'RawFilePath' to recurse into.
+followListMatching :: (RawFilePath -> Bool) -> RawFilePath -> IO [RawFilePath]
+followListMatching =
+    listAll'' True
+
+
+{- | List all files, directories & symlinks recursively.
+ Symlinks are not followed. See 'followList'.
+-}
+list :: RawFilePath -> IO [RawFilePath]
+list =
+    listAll'' False (const True)
+
+
+{- | List all files, directories & symlinks recursively.
+ Unlike 'list', symlinks are followed recursively as well.
+-}
+followList :: RawFilePath -> IO [RawFilePath]
+followList =
+    listAll'' True (const True)
+
+
+{- $custom
+ All /File Type Based Listing/ functions are based on top of this interface.
+ This part of API exposes exposes access for writing custom filtering functions.
+
+ All paths are tested for filter functions so unreadble files won't appear in the result list:
+
+ > PosixRecursive.listCustom PosixRecursive.defConf "i-dont-exist"
+ > >>> []
+
+ It's not possible to turn of this behaviour because this functions must get the 'FileStatus'
+ type which requires reading each entry.
+-}
+
+
+-- | Configuration arguments for 'listCustom'.
+data Conf = Conf
+    { -- | Filter paths algorithm should recurse into
+      filterPath :: !(RawFilePath -> Bool)
+    , -- | Test if file should be included in returned List
+      includeFile :: !(FileStatus -> RawFilePath -> IO Bool)
+    , -- | Follow symbolic links
+      followSymlinks :: !Bool
+    }
+
+
+{- | Default 'Conf'iguration.
+
+> listCustom defConf == listAccessible
+
+* Recurses into all Paths
+* Includes all file types
+* Does __not__ follow symlinks
+-}
+defConf :: Conf
+defConf =
+    Conf
+        { filterPath = const True
+        , includeFile = \_ _ -> pure True
+        , followSymlinks = False
+        }
+
+
+{-# INLINE listAccessible' #-}
+listAccessible' :: Conf -> RawFilePath -> IO [RawFilePath]
+listAccessible' Conf{..} path =
+    handle (\(_ :: IOError) -> pure []) $ do
+        file <- getFileStatus path
+        next <-
+            if Posix.isDirectory file
+                then do
+                    content <- listDir filterPath path
+                    unsafeInterleaveIO $ foldMapA (listAccessible' Conf{..}) content
+                else pure []
+
+        include <- includeFile file path
+        if include
+            then pure $ path : next
+            else pure next
+  where
+    {-# INLINE getFileStatus #-}
+    getFileStatus
+        | followSymlinks = Posix.getFileStatus
+        | otherwise = Posix.getSymbolicLinkStatus
+
+
+-- | Recursively list files using custom filters.
+listCustom :: Conf -> RawFilePath -> IO [RawFilePath]
+listCustom =
+    listAccessible'
+
+
+{- | Like 'list' but automatically filters out inacessible files like broken symlins
+or unreadable files and directories.
+-}
+listAccessible :: RawFilePath -> IO [RawFilePath]
+listAccessible =
+    listAccessible' defConf
+
+
+{- $type_based
+ Functions for listing specific file type. Reading the file type requires
+ ability to read the file.
+
+  __These function do not return broken symlinks and inacessible files__
+
+ Include test is applied even for the root entry (path past in as an argument).
+ This means that non existing paths are filtered.
+
+ > PosixRecursive.listDirectories "i-dont-exist"
+ > >>> []
+-}
+
+
+-- | List sub directories of given directory.
+listDirectories :: RawFilePath -> IO [RawFilePath]
+listDirectories =
+    listAccessible' defConf{includeFile = \file _ -> pure $ Posix.isDirectory file}
+
+
+-- | Lists only files (while recursing into directories still).
+listRegularFiles :: RawFilePath -> IO [RawFilePath]
+listRegularFiles =
+    listAccessible' defConf{includeFile = \file _ -> pure $ Posix.isRegularFile file}
+
+
+-- | Lists only symbolic links (while recursing into directories still).
+listSymbolicLinks :: RawFilePath -> IO [RawFilePath]
+listSymbolicLinks =
+    listAccessible' defConf{includeFile = \file _ -> pure $ Posix.isSymbolicLink file}
diff --git a/System/Posix/Recursive/ByteString/Unsafe.hs b/System/Posix/Recursive/ByteString/Unsafe.hs
new file mode 100644
--- /dev/null
+++ b/System/Posix/Recursive/ByteString/Unsafe.hs
@@ -0,0 +1,242 @@
+{- |
+ Module      : System.Posix.Recursive.ByteString.Unsafe-- Copyright   : (c) Marek Fajkus
+ License     : BSD3
+
+ Maintainer  : marek.faj@gmail.com
+
+ All modules profided by @unix-recursive@ expose similar API.
+ Make sure you're using the module wich best fits your needs based on:
+   - Working  with 'RawFilePath' (faster and more packed) or 'FilePath' (slower but easier to work with safely)
+   - Exception free (Default) or @Unsafe@ variants of functions
+
+ = Usage
+
+ This module is designed to be imported as @qualified@:
+
+ > import qualified System.Posix.Recursive.ByteString.Unsafe as PosixRecursive
+
+ __Results__
+
+ All functions return will return root path (the one they accept in argument) as a first item in the list:
+
+ > head <$> PosixRecursive.list "System"
+ > >>> "System"
+
+ Other than that, this library __provides no guarantees about the order in which files appear in the resulting list__
+ to make it possible to change the underlaying strategy in future versions.
+
+ __Laziness__
+
+ All IO operations are __guaranteed to be lazy and have constanct space characteristic__.
+ Only the IO required by lazy computation will be performed so for instance running code like:
+
+ > take 20 <$> PosixRecursive.listDirectories "/"
+
+ Will perform only minimal ammount of IO needed to collect 20 directories on a root partition
+-}
+module System.Posix.Recursive.ByteString.Unsafe (
+    -- * Basic Listing
+    -- $basic_listing
+    list,
+    followList,
+    listMatching,
+    followListMatching,
+
+    -- * File Type Based Listing
+    -- $type_based
+    listDirectories,
+    listRegularFiles,
+    listSymbolicLinks,
+
+    -- * Custom Listing
+    -- $custom
+    Conf (..),
+    defConf,
+    listCustom,
+) where
+
+import Control.Exception (bracket)
+import Data.Foldable (fold)
+import System.IO.Unsafe (unsafeInterleaveIO)
+
+import qualified Data.ByteString as BS
+import System.Posix.ByteString.FilePath (RawFilePath)
+
+import qualified System.Posix.Directory.ByteString as Posix
+import qualified System.Posix.Files.ByteString as Posix
+
+import System.Posix.Recursive.ByteString (Conf (..), defConf)
+
+
+-- Helpers
+
+foldMapA :: (Monoid b, Traversable t, Applicative f) => (a -> f b) -> t a -> f b
+foldMapA = (fmap fold .) . traverse
+
+
+{-# INLINE listDir #-}
+listDir :: (RawFilePath -> Bool) -> RawFilePath -> IO [RawFilePath]
+listDir predicate path =
+    bracket
+        (Posix.openDirStream path)
+        Posix.closeDirStream
+        (go [])
+  where
+    go :: [RawFilePath] -> Posix.DirStream -> IO [RawFilePath]
+    go acc dirp = do
+        e <- Posix.readDirStream dirp
+        if BS.null e
+            then pure acc
+            else
+                if e /= "." && e /= ".."
+                    then
+                        let fullPath = path <> "/" <> e
+                         in if predicate fullPath
+                                then go (fullPath : acc) dirp
+                                else go acc dirp
+                    else go acc dirp
+{- $basic_listing
+ Functions for listing contents of directory recursively.
+ These functions list all the content they encounter while traversing
+ the file system tree including directories, files, symlinks, broken symlinks.
+
+  __Functions from this module will throw 'IOError' if it can't open the directory__
+ (i.e. becacuse permission error or other process removing the given path).
+
+ Functions from this section is gurantee to always return the root path as a first element even
+ if this path does not exist.
+
+ > PosixRecursive.list "i-dont-exist"
+ > >>> ["i-dont-exist"]
+
+ In these cases the root path is considered the same way as symlink
+ to non existing location.
+-}
+
+
+{-# INLINE listAll' #-}
+listAll' :: Bool -> (RawFilePath -> Bool) -> RawFilePath -> IO [RawFilePath]
+listAll' followSymlinks predicate path =
+    do
+        file <- getFileStatus path
+        if Posix.isDirectory file
+            then do
+                content <- listDir predicate path
+
+                next <- unsafeInterleaveIO $ foldMapA (listAll' followSymlinks predicate) content
+                pure $ content ++ next
+            else pure []
+  where
+    {-# INLINE getFileStatus #-}
+    getFileStatus
+        | followSymlinks = Posix.getFileStatus
+        | otherwise = Posix.getSymbolicLinkStatus
+
+
+{-# INLINE listAll'' #-}
+listAll'' :: Bool -> (RawFilePath -> Bool) -> RawFilePath -> IO [RawFilePath]
+listAll'' followSymlinks predicate path =
+    (path :) <$> listAll' followSymlinks predicate path
+
+
+-- | Like 'list' but uses provided function to test in which 'RawFilePath' to recurse into.
+listMatching :: (RawFilePath -> Bool) -> RawFilePath -> IO [RawFilePath]
+listMatching =
+    listAll'' False
+
+
+-- | Like 'followList' but uses provided function to test in which 'RawFilePath' to recurse into.
+followListMatching :: (RawFilePath -> Bool) -> RawFilePath -> IO [RawFilePath]
+followListMatching =
+    listAll'' True
+
+
+{- | List all files, directories & symlinks recursively.
+ Symlinks are not followed. See 'followList'.
+-}
+list :: RawFilePath -> IO [RawFilePath]
+list =
+    listAll'' False (const True)
+
+
+{- | List all files, directories & symlinks recursively.
+ Unlike 'list', symlinks are followed recursively as well.
+-}
+followList :: RawFilePath -> IO [RawFilePath]
+followList =
+    listAll'' True (const True)
+
+
+{- $custom
+ All /File Type Based Listing/ functions are based on top of this interface.
+ This part of API exposes exposes access for writing custom filtering functions.
+
+ All paths are tested for filter functions so unreadble files won't appear in the result list:
+
+ > PosixRecursive.listCustom PosixRecursive.defConf "i-dont-exist"
+ > >>> []
+
+ It's not possible to turn of this behaviour because this functions must get the 'FileStatus'
+ type which requires reading each entry.
+-}
+
+
+{-# INLINE listAccessible' #-}
+listAccessible' :: Conf -> RawFilePath -> IO [RawFilePath]
+listAccessible' Conf{..} path =
+    do
+        file <- getFileStatus path
+        next <-
+            if Posix.isDirectory file
+                then do
+                    content <- listDir filterPath path
+                    unsafeInterleaveIO $ foldMapA (listAccessible' Conf{..}) content
+                else pure []
+
+        include <- includeFile file path
+        if include
+            then pure $ path : next
+            else pure next
+  where
+    {-# INLINE getFileStatus #-}
+    getFileStatus
+        | followSymlinks = Posix.getFileStatus
+        | otherwise = Posix.getSymbolicLinkStatus
+
+
+-- | Recursively list files using custom filters.
+listCustom :: Conf -> RawFilePath -> IO [RawFilePath]
+listCustom =
+    listAccessible'
+
+
+{- $type_based
+ Functions for listing specific file type. Reading the file type requires
+ ability to read the file.
+
+  __These functions will throw 'IOError' when tring to open unreadable file.__
+
+ Include test is applied even for the root entry (path past in as an argument).
+ This means that non existing paths are filtered.
+
+ > PosixRecursive.listDirectories "i-dont-exist"
+ > >>> []
+-}
+
+
+-- | List sub directories of given directory.
+listDirectories :: RawFilePath -> IO [RawFilePath]
+listDirectories =
+    listAccessible' defConf{includeFile = \file _ -> pure $ Posix.isDirectory file}
+
+
+-- | Lists only files (while recursing into directories still).
+listRegularFiles :: RawFilePath -> IO [RawFilePath]
+listRegularFiles =
+    listAccessible' defConf{includeFile = \file _ -> pure $ Posix.isRegularFile file}
+
+
+-- | Lists only symbolic links (while recursing into directories still).
+listSymbolicLinks :: RawFilePath -> IO [RawFilePath]
+listSymbolicLinks =
+    listAccessible' defConf{includeFile = \file _ -> pure $ Posix.isSymbolicLink file}
diff --git a/System/Posix/Recursive/Unsafe.hs b/System/Posix/Recursive/Unsafe.hs
new file mode 100644
--- /dev/null
+++ b/System/Posix/Recursive/Unsafe.hs
@@ -0,0 +1,238 @@
+{- |
+ Module      : System.Posix.Recursive.Unsafe-- Copyright   : (c) Marek Fajkus
+ License     : BSD3
+
+ Maintainer  : marek.faj@gmail.com
+
+ All modules profided by @unix-recursive@ expose similar API.
+ Make sure you're using the module wich best fits your needs based on:
+   - Working  with 'RawFilePath' (faster and more packed) or 'FilePath' (slower but easier to work with safely)
+   - Exception free (Default) or @Unsafe@ variants of functions
+
+ = Usage
+
+ This module is designed to be imported as @qualified@:
+
+ > import qualified System.Posix.Recursive.Unsafe as PosixRecursive
+
+ __Results__
+
+ All functions return will return root path (the one they accept in argument) as a first item in the list:
+
+ > head <$> PosixRecursive.list "System"
+ > >>> "System"
+
+ Other than that, this library __provides no guarantees about the order in which files appear in the resulting list__
+ to make it possible to change the underlaying strategy in future versions.
+
+ __Laziness__
+
+ All IO operations are __guaranteed to be lazy and have constanct space characteristic__.
+ Only the IO required by lazy computation will be performed so for instance running code like:
+
+ > take 20 <$> PosixRecursive.listDirectories "/"
+
+ Will perform only minimal ammount of IO needed to collect 20 directories on a root partition
+-}
+module System.Posix.Recursive.Unsafe (
+    -- * Basic Listing
+    -- $basic_listing
+    list,
+    followList,
+    listMatching,
+    followListMatching,
+
+    -- * File Type Based Listing
+    -- $type_based
+    listDirectories,
+    listRegularFiles,
+    listSymbolicLinks,
+
+    -- * Custom Listing
+    -- $custom
+    Conf (..),
+    defConf,
+    listCustom,
+) where
+
+import Control.Exception (bracket)
+import Data.Foldable (fold)
+import System.IO.Unsafe (unsafeInterleaveIO)
+
+import qualified System.Posix.Directory as Posix
+import qualified System.Posix.Files as Posix
+import System.Posix.Recursive (Conf (..), defConf)
+
+
+-- Helpers
+
+foldMapA :: (Monoid b, Traversable t, Applicative f) => (a -> f b) -> t a -> f b
+foldMapA = (fmap fold .) . traverse
+
+
+{-# INLINE listDir #-}
+listDir :: (FilePath -> Bool) -> FilePath -> IO [FilePath]
+listDir predicate path =
+    bracket
+        (Posix.openDirStream path)
+        Posix.closeDirStream
+        (go [])
+  where
+    go :: [FilePath] -> Posix.DirStream -> IO [FilePath]
+    go acc dirp = do
+        e <- Posix.readDirStream dirp
+        if null e
+            then pure acc
+            else
+                if e /= "." && e /= ".."
+                    then
+                        let fullPath = path <> "/" <> e
+                         in if predicate fullPath
+                                then go (fullPath : acc) dirp
+                                else go acc dirp
+                    else go acc dirp
+{- $basic_listing
+ Functions for listing contents of directory recursively.
+ These functions list all the content they encounter while traversing
+ the file system tree including directories, files, symlinks, broken symlinks.
+
+  __Functions from this module will throw 'IOError' if it can't open the directory__
+ (i.e. becacuse permission error or other process removing the given path).
+
+ Functions from this section is gurantee to always return the root path as a first element even
+ if this path does not exist.
+
+ > PosixRecursive.list "i-dont-exist"
+ > >>> ["i-dont-exist"]
+
+ In these cases the root path is considered the same way as symlink
+ to non existing location.
+-}
+
+
+{-# INLINE listAll' #-}
+listAll' :: Bool -> (FilePath -> Bool) -> FilePath -> IO [FilePath]
+listAll' followSymlinks predicate path =
+    do
+        file <- getFileStatus path
+        if Posix.isDirectory file
+            then do
+                content <- listDir predicate path
+
+                next <- unsafeInterleaveIO $ foldMapA (listAll' followSymlinks predicate) content
+                pure $ content ++ next
+            else pure []
+  where
+    {-# INLINE getFileStatus #-}
+    getFileStatus
+        | followSymlinks = Posix.getFileStatus
+        | otherwise = Posix.getSymbolicLinkStatus
+
+
+{-# INLINE listAll'' #-}
+listAll'' :: Bool -> (FilePath -> Bool) -> FilePath -> IO [FilePath]
+listAll'' followSymlinks predicate path =
+    (path :) <$> listAll' followSymlinks predicate path
+
+
+-- | Like 'list' but uses provided function to test in which 'FilePath' to recurse into.
+listMatching :: (FilePath -> Bool) -> FilePath -> IO [FilePath]
+listMatching =
+    listAll'' False
+
+
+-- | Like 'followList' but uses provided function to test in which 'FilePath' to recurse into.
+followListMatching :: (FilePath -> Bool) -> FilePath -> IO [FilePath]
+followListMatching =
+    listAll'' True
+
+
+{- | List all files, directories & symlinks recursively.
+ Symlinks are not followed. See 'followList'.
+-}
+list :: FilePath -> IO [FilePath]
+list =
+    listAll'' False (const True)
+
+
+{- | List all files, directories & symlinks recursively.
+ Unlike 'list', symlinks are followed recursively as well.
+-}
+followList :: FilePath -> IO [FilePath]
+followList =
+    listAll'' True (const True)
+
+
+{- $custom
+ All /File Type Based Listing/ functions are based on top of this interface.
+ This part of API exposes exposes access for writing custom filtering functions.
+
+ All paths are tested for filter functions so unreadble files won't appear in the result list:
+
+ > PosixRecursive.listCustom PosixRecursive.defConf "i-dont-exist"
+ > >>> []
+
+ It's not possible to turn of this behaviour because this functions must get the 'FileStatus'
+ type which requires reading each entry.
+-}
+
+
+{-# INLINE listAccessible' #-}
+listAccessible' :: Conf -> FilePath -> IO [FilePath]
+listAccessible' Conf{..} path =
+    do
+        file <- getFileStatus path
+        next <-
+            if Posix.isDirectory file
+                then do
+                    content <- listDir filterPath path
+                    unsafeInterleaveIO $ foldMapA (listAccessible' Conf{..}) content
+                else pure []
+
+        include <- includeFile file path
+        if include
+            then pure $ path : next
+            else pure next
+  where
+    {-# INLINE getFileStatus #-}
+    getFileStatus
+        | followSymlinks = Posix.getFileStatus
+        | otherwise = Posix.getSymbolicLinkStatus
+
+
+-- | Recursively list files using custom filters.
+listCustom :: Conf -> FilePath -> IO [FilePath]
+listCustom =
+    listAccessible'
+
+
+{- $type_based
+ Functions for listing specific file type. Reading the file type requires
+ ability to read the file.
+
+  __These functions will throw 'IOError' when tring to open unreadable file.__
+
+ Include test is applied even for the root entry (path past in as an argument).
+ This means that non existing paths are filtered.
+
+ > PosixRecursive.listDirectories "i-dont-exist"
+ > >>> []
+-}
+
+
+-- | List sub directories of given directory.
+listDirectories :: FilePath -> IO [FilePath]
+listDirectories =
+    listAccessible' defConf{includeFile = \file _ -> pure $ Posix.isDirectory file}
+
+
+-- | Lists only files (while recursing into directories still).
+listRegularFiles :: FilePath -> IO [FilePath]
+listRegularFiles =
+    listAccessible' defConf{includeFile = \file _ -> pure $ Posix.isRegularFile file}
+
+
+-- | Lists only symbolic links (while recursing into directories still).
+listSymbolicLinks :: FilePath -> IO [FilePath]
+listSymbolicLinks =
+    listAccessible' defConf{includeFile = \file _ -> pure $ Posix.isSymbolicLink file}
diff --git a/bench/Bench.hs b/bench/Bench.hs
new file mode 100644
--- /dev/null
+++ b/bench/Bench.hs
@@ -0,0 +1,43 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main (main) where
+
+import Criterion.Main
+
+import System.Directory.Recursive as DirTraverse
+import qualified System.Posix.Recursive as String
+import qualified System.Posix.Recursive.ByteString as ByteString
+
+
+main :: IO ()
+main =
+    defaultMain
+        [ bgroup
+            "compare different api groups"
+            [ bgroup
+                "ByteString"
+                [ bench "filtering base on path" $ nfIO $ ByteString.list "."
+                , bench "filter by path and file status" $ nfIO $ ByteString.listAccessible "."
+                ]
+            , bgroup
+                "String"
+                [ bench "filtering base on path" $ nfIO $ String.list "."
+                , bench "filter by path and file status" $ nfIO $ String.listAccessible "."
+                ]
+            ]
+        , bgroup
+            "compare with dir traverse"
+            [ bgroup
+                "Evaluate whole list"
+                [ bench "unix-recursive String" $ nfIO $ String.followList "."
+                , bench "unix-recursive ByteString" $ nfIO $ ByteString.followList "."
+                , bench "dir-traverse" $ nfIO $ DirTraverse.getDirRecursive "."
+                ]
+            , bgroup
+                "Test laziness"
+                [ bench "unix-recursive String" $ nfIO (head . tail <$> String.followList ".")
+                , bench "unix-recursive ByteString" $ nfIO (head . tail <$> ByteString.followList ".")
+                , bench "dir-traverse" $ nfIO ((head . tail) <$> DirTraverse.getDirRecursive ".")
+                ]
+            ]
+        ]
diff --git a/bin/dir-traverse.hs b/bin/dir-traverse.hs
new file mode 100644
--- /dev/null
+++ b/bin/dir-traverse.hs
@@ -0,0 +1,11 @@
+module Main where
+
+import System.Directory.Recursive as Lib
+import System.Environment (getArgs)
+
+
+main :: IO ()
+main = do
+    [path] <- getArgs
+    all <- Lib.getDirRecursive path
+    print $ length all
diff --git a/bin/dirstream.hs b/bin/dirstream.hs
new file mode 100644
--- /dev/null
+++ b/bin/dirstream.hs
@@ -0,0 +1,32 @@
+module Main where
+
+import Control.Applicative
+import Data.DirStream
+import Data.String
+import qualified Filesystem.Path as F
+import Pipes
+import Pipes.Prelude hiding (print)
+import Pipes.Safe
+import System.Environment
+import Prelude hiding (length)
+
+
+dirstreamGet :: F.FilePath -> ListT (SafeT IO) F.FilePath
+dirstreamGet path = do
+    child <- childOf path
+    isDir <- liftIO $ isDirectory child
+    if isDir
+        then pure child <|> descendentOf child
+        else pure child
+
+
+main :: IO ()
+main = do
+    [path] <- getArgs
+
+    count <-
+        runSafeT $
+            runEffect $
+                (length (every (descendentOf (fromString path))))
+
+    print count
diff --git a/bin/unix-recursive-bytestring.hs b/bin/unix-recursive-bytestring.hs
new file mode 100644
--- /dev/null
+++ b/bin/unix-recursive-bytestring.hs
@@ -0,0 +1,13 @@
+module Main where
+
+import qualified Data.ByteString.UTF8 as BS
+import System.Environment (getArgs)
+import qualified System.Posix.Recursive.ByteString as Lib
+
+
+main :: IO ()
+main = do
+    [path'] <- getArgs
+    let path = BS.fromString path'
+    all <- Lib.followList path
+    print $ length all
diff --git a/bin/unix-recursive-string.hs b/bin/unix-recursive-string.hs
new file mode 100644
--- /dev/null
+++ b/bin/unix-recursive-string.hs
@@ -0,0 +1,11 @@
+module Main where
+
+import System.Environment (getArgs)
+import qualified System.Posix.Recursive as Lib
+
+
+main :: IO ()
+main = do
+    [path] <- getArgs
+    all <- Lib.followList path
+    print $ length all
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,82 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+import Data.List (sort)
+
+import Spec.Listing
+import Test.Hspec --(anyException, context, hspec, it, shouldThrow)
+
+import System.IO.Error (isPermissionError)
+import System.Posix.ByteString.FilePath (RawFilePath)
+
+import qualified Data.ByteString as BS
+import qualified System.Posix.Recursive as String
+import qualified System.Posix.Recursive.ByteString as ByteString
+import qualified System.Posix.Recursive.ByteString.Unsafe as UnsafeBS
+import qualified System.Posix.Recursive.Unsafe as Unsafe
+
+
+-- Helper
+isSuffixOf :: String -> String -> Bool
+isSuffixOf needle haystack
+    | needleLen > hayLen = False
+    | otherwise = needle == drop (hayLen - needleLen) haystack
+  where
+    needleLen = length needle
+    hayLen = length haystack
+
+
+instance DirectoryListing FilePath where
+    list = String.list
+    followList = String.followList
+    listMatching = String.listMatching
+    followListMatching = String.followListMatching
+    listAccessible = String.listAccessible
+    listDirectories = String.listDirectories
+    listRegularFiles = String.listRegularFiles
+    listSymbolicLinks = String.listSymbolicLinks
+    listCustom filterPath includeFile =
+        String.listCustom
+            String.defConf
+                { String.filterPath = filterPath
+                , String.includeFile = includeFile
+                }
+
+
+instance DirectoryListing RawFilePath where
+    list = ByteString.list
+    followList = ByteString.followList
+    listMatching = ByteString.listMatching
+    followListMatching = ByteString.followListMatching
+    listDirectories = ByteString.listDirectories
+    listAccessible = ByteString.listAccessible
+    listRegularFiles = ByteString.listRegularFiles
+    listSymbolicLinks = ByteString.listSymbolicLinks
+    listCustom filterPath includeFile =
+        ByteString.listCustom
+            ByteString.defConf
+                { ByteString.filterPath = filterPath
+                , ByteString.includeFile = includeFile
+                }
+
+
+main :: IO ()
+main =
+    hspec $ do
+        context "System.Posix.Recursive" $ spec isSuffixOf
+
+        context "System.Posix.Recursive.Unsafe" $
+            it "should throw" $ do
+                let exp = do
+                        res <- Unsafe.list "test/workdir"
+                        print $ length res
+                exp `shouldThrow` isPermissionError
+
+        context "System.Posix.Recursive.ByteString" $ spec BS.isSuffixOf
+
+        context "System.Posix.Recursive.ByteString.Unsafe" $
+            it "should throw" $ do
+                let exp = do
+                        res <- UnsafeBS.list "test/workdir"
+                        print $ length res
+                exp `shouldThrow` isPermissionError
diff --git a/test/Spec/Listing.hs b/test/Spec/Listing.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec/Listing.hs
@@ -0,0 +1,242 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Spec.Listing (spec, DirectoryListing (..)) where
+
+import Data.List (sort)
+import Data.String
+import System.Posix.Files (FileStatus)
+import qualified System.Posix.Files as Posix
+import Test.Hspec
+
+
+class (Show a, Ord a, IsString a) => DirectoryListing a where
+    list :: a -> IO [a]
+    followList :: a -> IO [a]
+    listMatching :: (a -> Bool) -> a -> IO [a]
+    followListMatching :: (a -> Bool) -> a -> IO [a]
+    listAccessible :: a -> IO [a]
+    listDirectories :: a -> IO [a]
+    listRegularFiles :: a -> IO [a]
+    listSymbolicLinks :: a -> IO [a]
+    listCustom :: (a -> Bool) -> (FileStatus -> a -> IO Bool) -> a -> IO [a]
+
+
+spec :: forall a. DirectoryListing a => (a -> a -> Bool) -> Spec
+spec filter = do
+    context "Not following symlinks" $ do
+        describe "listEverything" $ do
+            it "returns expected list of everything in workdir" $ do
+                res :: [a] <- list "test/workdir"
+                let expected =
+                        [ "test/workdir"
+                        , "test/workdir/dir1"
+                        , "test/workdir/dir1/roots-dir1"
+                        , "test/workdir/dir1/roots-dir1/roots-file1"
+                        , "test/workdir/dir1/sub1"
+                        , "test/workdir/dir1/sub1/file1"
+                        , "test/workdir/dir1/sub1/file2"
+                        , "test/workdir/dir1/sub2"
+                        , "test/workdir/dir1/sub2/file1"
+                        , "test/workdir/dir1/sub2/file2"
+                        , "test/workdir/dir1/sub2/file3"
+                        , "test/workdir/dir1/sub2/roots-file1"
+                        , "test/workdir/dir2"
+                        , "test/workdir/dir2/file1"
+                        , "test/workdir/dir2/sym1"
+                        , "test/workdir/dir2/sym2"
+                        , "test/workdir/dir3"
+                        , "test/workdir/dir3/sub1"
+                        , "test/workdir/dir3/sub1/file1"
+                        , "test/workdir/dir3/sub2"
+                        , "test/workdir/dir3/sub2/broken-sym1"
+                        , "test/workdir/file1"
+                        , "test/workdir/file2"
+                        , "test/workdir/only-roots-dir1"
+                        ]
+                res `shouldMatchList` expected
+
+        describe "listAll" $ do
+            it "returns list excluding files & dirs ending with `3`" $ do
+                res :: [a] <- listMatching (not . filter "3") "test/workdir"
+                let expected =
+                        [ "test/workdir"
+                        , "test/workdir/dir1"
+                        , "test/workdir/dir1/roots-dir1"
+                        , "test/workdir/dir1/roots-dir1/roots-file1"
+                        , "test/workdir/dir1/sub1"
+                        , "test/workdir/dir1/sub1/file1"
+                        , "test/workdir/dir1/sub1/file2"
+                        , "test/workdir/dir1/sub2"
+                        , "test/workdir/dir1/sub2/file1"
+                        , "test/workdir/dir1/sub2/file2"
+                        , "test/workdir/dir1/sub2/roots-file1"
+                        , "test/workdir/dir2"
+                        , "test/workdir/dir2/file1"
+                        , "test/workdir/dir2/sym1"
+                        , "test/workdir/dir2/sym2"
+                        , "test/workdir/file1"
+                        , "test/workdir/file2"
+                        , "test/workdir/only-roots-dir1"
+                        ]
+                res `shouldMatchList` expected
+
+        describe "listEverythingAccessible" $ do
+            it "returns everything that is accessible in workdir" $ do
+                res :: [a] <- listAccessible "test/workdir"
+                let expected =
+                        [ "test/workdir"
+                        , "test/workdir/dir1"
+                        , "test/workdir/dir1/roots-dir1"
+                        , "test/workdir/dir1/roots-dir1/roots-file1"
+                        , "test/workdir/dir1/sub1"
+                        , "test/workdir/dir1/sub1/file1"
+                        , "test/workdir/dir1/sub1/file2"
+                        , "test/workdir/dir1/sub2"
+                        , "test/workdir/dir1/sub2/file1"
+                        , "test/workdir/dir1/sub2/file2"
+                        , "test/workdir/dir1/sub2/file3"
+                        , "test/workdir/dir1/sub2/roots-file1"
+                        , "test/workdir/dir2"
+                        , "test/workdir/dir2/file1"
+                        , "test/workdir/dir2/sym1"
+                        , "test/workdir/dir2/sym2"
+                        , "test/workdir/dir3"
+                        , "test/workdir/dir3/sub1"
+                        , "test/workdir/dir3/sub1/file1"
+                        , "test/workdir/dir3/sub2"
+                        , "test/workdir/dir3/sub2/broken-sym1"
+                        , "test/workdir/file1"
+                        , "test/workdir/file2"
+                        ]
+                res `shouldMatchList` expected
+
+        describe "listDirectories" $ do
+            it "returns only directories" $ do
+                res :: [a] <- listDirectories "test/workdir"
+                let expected =
+                        [ "test/workdir"
+                        , "test/workdir/dir1"
+                        , "test/workdir/dir1/roots-dir1"
+                        , "test/workdir/dir1/sub1"
+                        , "test/workdir/dir1/sub2"
+                        , "test/workdir/dir2"
+                        , "test/workdir/dir3"
+                        , "test/workdir/dir3/sub1"
+                        , "test/workdir/dir3/sub2"
+                        ]
+                res `shouldMatchList` expected
+
+        describe "listFiles" $ do
+            it "returns only files" $ do
+                res :: [a] <- listRegularFiles "test/workdir"
+                let expected =
+                        [ "test/workdir/dir1/roots-dir1/roots-file1"
+                        , "test/workdir/dir1/sub1/file1"
+                        , "test/workdir/dir1/sub1/file2"
+                        , "test/workdir/dir1/sub2/file1"
+                        , "test/workdir/dir1/sub2/file2"
+                        , "test/workdir/dir1/sub2/file3"
+                        , "test/workdir/dir1/sub2/roots-file1"
+                        , "test/workdir/dir2/file1"
+                        , "test/workdir/dir3/sub1/file1"
+                        , "test/workdir/file1"
+                        , "test/workdir/file2"
+                        ]
+                res `shouldMatchList` expected
+
+        describe "listSymbolicLinks" $ do
+            it "returns only symlinks" $ do
+                res :: [a] <- listSymbolicLinks "test/workdir"
+                let expected =
+                        [ "test/workdir/dir2/sym1"
+                        , "test/workdir/dir2/sym2"
+                        , "test/workdir/dir3/sub2/broken-sym1"
+                        ]
+                res `shouldMatchList` expected
+
+        describe "listCustom" $ do
+            it "return everything except for symlinks and not within dir1" $ do
+                res :: [a] <-
+                    listCustom
+                        (not . filter "dir1")
+                        (\file _ -> pure $ not $ Posix.isSymbolicLink file)
+                        "test/workdir"
+                let expected =
+                        [ "test/workdir"
+                        , "test/workdir/dir2"
+                        , "test/workdir/dir2/file1"
+                        , "test/workdir/dir3"
+                        , "test/workdir/dir3/sub1"
+                        , "test/workdir/dir3/sub1/file1"
+                        , "test/workdir/dir3/sub2"
+                        , "test/workdir/file1"
+                        , "test/workdir/file2"
+                        ]
+                res `shouldMatchList` expected
+
+    context "Following symlinks" $ do
+        describe "followListEverything" $ do
+            it "returns expected list of everything in workdir" $ do
+                res :: [a] <- followList "test/workdir"
+                let expected =
+                        [ "test/workdir"
+                        , "test/workdir/dir1"
+                        , "test/workdir/dir1/roots-dir1"
+                        , "test/workdir/dir1/roots-dir1/roots-file1"
+                        , "test/workdir/dir1/sub1"
+                        , "test/workdir/dir1/sub1/file1"
+                        , "test/workdir/dir1/sub1/file2"
+                        , "test/workdir/dir1/sub2"
+                        , "test/workdir/dir1/sub2/file1"
+                        , "test/workdir/dir1/sub2/file2"
+                        , "test/workdir/dir1/sub2/file3"
+                        , "test/workdir/dir1/sub2/roots-file1"
+                        , "test/workdir/dir2"
+                        , "test/workdir/dir2/file1"
+                        , "test/workdir/dir2/sym1"
+                        , "test/workdir/dir2/sym2"
+                        , "test/workdir/dir2/sym2/sub1"
+                        , "test/workdir/dir2/sym2/sub1/file1"
+                        , "test/workdir/dir2/sym2/sub2"
+                        , "test/workdir/dir2/sym2/sub2/broken-sym1"
+                        , "test/workdir/dir3"
+                        , "test/workdir/dir3/sub1"
+                        , "test/workdir/dir3/sub1/file1"
+                        , "test/workdir/dir3/sub2"
+                        , "test/workdir/dir3/sub2/broken-sym1"
+                        , "test/workdir/file1"
+                        , "test/workdir/file2"
+                        , "test/workdir/only-roots-dir1"
+                        ]
+                res `shouldMatchList` expected
+
+        describe "followListAll" $ do
+            it "returns list excluding files & dirs ending with `3`" $ do
+                res <- followListMatching (not . filter "3") "test/workdir"
+                let expected =
+                        [ "test/workdir"
+                        , "test/workdir/dir1"
+                        , "test/workdir/dir1/roots-dir1"
+                        , "test/workdir/dir1/roots-dir1/roots-file1"
+                        , "test/workdir/dir1/sub1"
+                        , "test/workdir/dir1/sub1/file1"
+                        , "test/workdir/dir1/sub1/file2"
+                        , "test/workdir/dir1/sub2"
+                        , "test/workdir/dir1/sub2/file1"
+                        , "test/workdir/dir1/sub2/file2"
+                        , "test/workdir/dir1/sub2/roots-file1"
+                        , "test/workdir/dir2"
+                        , "test/workdir/dir2/file1"
+                        , "test/workdir/dir2/sym1"
+                        , "test/workdir/dir2/sym2"
+                        , "test/workdir/dir2/sym2/sub1"
+                        , "test/workdir/dir2/sym2/sub1/file1"
+                        , "test/workdir/dir2/sym2/sub2"
+                        , "test/workdir/dir2/sym2/sub2/broken-sym1"
+                        , "test/workdir/file1"
+                        , "test/workdir/file2"
+                        , "test/workdir/only-roots-dir1"
+                        ]
+                res `shouldMatchList` expected
diff --git a/unix-recursive.cabal b/unix-recursive.cabal
new file mode 100644
--- /dev/null
+++ b/unix-recursive.cabal
@@ -0,0 +1,118 @@
+cabal-version:       >=1.12
+
+name:                unix-recursive
+version:             0.1.0.0
+synopsis:            Fast and flexible primitives for recursive file system IO on Posix systems
+description:
+    Blazingly fast functions for recursive file system operations.
+    Utilizing lazy IO for constant space & computation efficiant bindigns to Posix dirstream.h api.
+
+bug-reports:         https://github.com/turboMaCk/unix-recursive/issues
+license:             BSD3
+license-file:        LICENSE
+author:              Marek Fajkus <marek.faj@gmail.com>
+maintainer:          Marek Fajkus <marek.faj@gmail.com>
+copyright:           2021 Marek Fajkus
+stability:           experimental
+category:            System, Filesystem
+build-type:          Simple
+extra-source-files:  README.md
+                   , ChangeLog.md
+homepage:            https://github.com/turboMaCk/unix-recursive
+
+source-repository head
+  type: git
+  location: https://github.com/turboMaCk/unix-recursive
+
+Flag bin
+  default: False
+  manual: True
+
+library
+    exposed-modules:      System.Posix.Recursive
+                        , System.Posix.Recursive.Unsafe
+                        , System.Posix.Recursive.ByteString
+                        , System.Posix.Recursive.ByteString.Unsafe
+    other-modules:        Paths_unix_recursive
+    hs-source-dirs:       .
+    build-depends:        base >=4.7 && <5
+                        , unix
+                        , bytestring
+    default-language:     Haskell2010
+    default-extensions:   OverloadedStrings
+                        , RecordWildCards
+                        , ScopedTypeVariables
+
+executable dir-traverse-bin
+    main-is:            dir-traverse.hs
+    hs-source-dirs:     bin
+    ghc-options:        -threaded -rtsopts -with-rtsopts=-N -Wall
+    build-depends:      base >=4.7 && <5
+                      , dir-traverse == 0.2.3.0
+    default-language:   Haskell2010
+    if !flag(bin)
+        buildable: False
+
+executable dirstream-bin
+    main-is:            dirstream.hs
+    hs-source-dirs:     bin
+    ghc-options:        -threaded -rtsopts -with-rtsopts=-N -Wall
+    build-depends:      base >=4.7 && <5
+                      , dirstream
+                      , pipes
+                      , pipes-safe
+                      , system-filepath
+                      , directory
+    default-language:   Haskell2010
+    if !flag(bin)
+        buildable: False
+
+executable unix-recursive-string-bin
+    main-is:            unix-recursive-string.hs
+    other-modules:      Paths_unix_recursive
+    hs-source-dirs:     bin
+    ghc-options:        -threaded -rtsopts -with-rtsopts=-N -Wall
+    build-depends:      base >=4.7 && <5
+                      , unix-recursive
+                      , unix
+    default-language:   Haskell2010
+    if !flag(bin)
+        buildable: False
+
+executable unix-recursive-bytestring-bin
+    main-is:            unix-recursive-bytestring.hs
+    other-modules:      Paths_unix_recursive
+    hs-source-dirs:     bin
+    ghc-options:        -threaded -rtsopts -with-rtsopts=-N -Wall
+    build-depends:      base >=4.7 && <5
+                      , unix-recursive
+                      , unix
+                      , utf8-string
+    default-language:   Haskell2010
+    if !flag(bin)
+        buildable: False
+
+benchmark unix-recursive-bench
+    type:               exitcode-stdio-1.0
+    main-is:            Bench.hs
+    hs-source-dirs:     bench
+    default-language:   Haskell2010
+    ghc-options:        -Wall
+    build-depends:      base >=4.7 && <5
+                      , unix-recursive
+                      , criterion
+                      , dir-traverse == 0.2.3.0
+
+test-suite unix-recursive-test
+    type:               exitcode-stdio-1.0
+    main-is:            Spec.hs
+    other-modules:      Paths_unix_recursive
+                      , Spec.Listing
+    hs-source-dirs:     test
+    ghc-options:        -threaded -rtsopts -with-rtsopts=-N -Wall
+    build-depends:      base >=4.7 && <5
+                      , unix-recursive
+                      , hspec
+                      , bytestring
+                      , unix
+    default-language:   Haskell2010
