diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,3 @@
+## 0.0.1
+
+* Initial release.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Shlok Datye (c) 2024
+
+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 Shlok Datye 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,36 @@
+# streamly-zip
+
+Stream data from zip archives using the Haskell [streamly](https://hackage.haskell.org/package/streamly) library.
+
+## Comparison with streamly-archive
+
+This library was created because libarchive (which [streamly-archive](https://hackage.haskell.org/package/streamly-archive) relies on) does not seem to have support for jumping to specific files even when the format supports it.
+
+## Requirements
+
+Install libzip on your system.
+
+* Debian Linux: `sudo apt-get install libzip-dev`.
+* macOS: `brew install libzip`.
+
+## Quick start
+
+```haskell
+module Main where
+
+import qualified Data.ByteString as B
+import Data.Function
+import qualified Streamly.Data.Fold as F
+import qualified Streamly.Data.Stream.Prelude as S
+import Streamly.External.Zip
+
+main :: IO ()
+main = do
+  -- Obtain an archive.
+  z <- openZip "/path/to/archive.zip" []
+
+  -- Output a particular file to stdout.
+  S.unfold unfoldFileAtPath (z, [], "file.txt")
+    & S.mapM B.putStr
+    & S.fold F.drain
+```
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/src/Streamly/External/Zip.hs b/src/Streamly/External/Zip.hs
new file mode 100644
--- /dev/null
+++ b/src/Streamly/External/Zip.hs
@@ -0,0 +1,23 @@
+-- The documentation in this module has at times been copy-pasted directly from the [libzip
+-- documentation](https://libzip.org/documentation/).
+module Streamly.External.Zip
+  ( -- ** Open archive
+    Zip,
+    OpenFlag (..),
+    openZip,
+
+    -- ** Archive information
+    getNumEntries,
+
+    -- ** Paths
+    PathFlag (..),
+    getPathByIndex,
+
+    -- ** Streamly
+    GetFileFlag (..),
+    unfoldFileAtPath,
+    unfoldFileAtIndex,
+  )
+where
+
+import Streamly.External.Zip.Internal
diff --git a/src/Streamly/External/Zip/Internal.hs b/src/Streamly/External/Zip/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Streamly/External/Zip/Internal.hs
@@ -0,0 +1,243 @@
+{-# LANGUAGE TypeApplications #-}
+
+module Streamly.External.Zip.Internal where
+
+import Control.Exception
+import Control.Monad
+import Control.Monad.IO.Class
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Char8 as BC
+import Data.List
+import Data.Map.Strict (Map)
+import qualified Data.Map.Strict as M
+import Data.Maybe
+import Foreign
+import Foreign.C.String
+import Foreign.C.Types
+import Streamly.Data.Unfold (Unfold)
+import Streamly.External.Zip.Internal.Error
+import Streamly.External.Zip.Internal.Foreign
+import Streamly.Internal.Data.IOFinalizer
+import qualified Streamly.Internal.Data.Unfold as U
+import Text.Printf
+
+-- | A zip archive.
+newtype Zip
+  = -- A ForeignPtr works because zip_discard() returns void.
+    Zip (ForeignPtr Zip_t)
+
+-- (*) Certain libzip functionality (e.g., flags) has been commented out because it is currently not
+-- applicable for this library, e.g., because this library is currently read-only.
+
+data OpenFlag
+  = -- Most libzip flags not included; see (*).
+
+    -- | Perform additional stricter consistency checks on the archive, and error if they fail.
+    O_CHECKCONS
+  deriving (Eq, Ord)
+
+-- | /Internal/.
+openFlags :: Map OpenFlag CInt
+openFlags =
+  M.fromList
+    [ (O_CHECKCONS, zip_checkcons)
+    -- (O_CREATE, zip_create), -- See (*).
+    -- (O_EXCL, zip_excl),
+    -- (O_TRUNCATE, zip_truncate)
+    -- (O_RDONLY, zip_rdonly)
+    ]
+
+-- | Opens the zip archive at the given file path.
+--
+-- /Warning/: To satisfy low-level libzip requirements, please use each 'Zip' from one thread
+-- only—or make sure to synchronize its use. Note that it is perfectly fine to open multiple 'Zip's
+-- for a single zip file on disk.
+openZip :: FilePath -> [OpenFlag] -> IO Zip
+openZip fp flags =
+  -- This library is currently read-only; always open the archive in read-only mode; see (*).
+  let flags' = zip_rdonly .|. combineFlags openFlags flags
+   in withCString fp $ \fpc -> alloca $ \errp -> mask_ $ do
+        zipp <- c_zip_open fpc flags' errp
+        if zipp == nullPtr
+          then do
+            err <- libzipErrToString =<< peek errp
+            throwError "openZip" err
+          else Zip <$> newForeignPtr c_zip_discard_ptr zipp
+
+-- See (*).
+-- data NumEntriesFlag
+--   = -- | The original number of entries is returned.
+--     NE_FL_UNCHANGED
+--   deriving (Eq, Ord)
+
+-- numEntriesFlags :: Map NumEntriesFlag Zip_flags_t
+-- numEntriesFlags =
+--   M.fromList
+--     [(NEF_FL_UNCHANGED, zip_fl_unchanged)]
+
+-- getNumEntries :: Zip -> [NumEntriesFlag] -> IO Int
+
+-- | Gets the number of entries in the given archive.
+getNumEntries :: Zip -> IO Int
+getNumEntries (Zip zipfp) =
+  let flags' = 0 -- combineFlags numEntriesFlags flags; see (*).
+   in do
+        num <- withForeignPtr zipfp $ \zipp -> c_zip_get_num_entries zipp flags'
+        if num < 0
+          then -- c_zip_get_num_entries should not return -1 here, for zipp is known to be non-NULL.
+            throwError "getNumEntries" "unexpected"
+          else return $ fromIntegral num
+
+data PathFlag
+  = --
+    --   -- | The original unchanged filename is returned. -- See (*).
+    --   P_FL_UNCHANGED
+
+    -- | Return the unmodified names as it is in the ZIP archive.
+    P_FL_ENC_RAW
+  | -- | (Default.) Guess the encoding of the name in the ZIP archive and convert it to UTF-8, if
+    -- necessary. (Only CP-437 and UTF-8 are recognized.)
+    P_FL_ENC_GUESS
+  | -- | Follow the ZIP specification and expect CP-437 encoded names in the ZIP archive (except if
+    -- they are explicitly marked as UTF-8). Convert it to UTF-8.
+    P_FL_ENC_STRICT
+  deriving (Eq, Ord)
+
+-- | /Internal/.
+pathFlags :: Map PathFlag Zip_flags_t
+pathFlags =
+  M.fromList
+    [ -- (P_FL_UNCHANGED, zip_fl_unchanged) -- See (*).
+      (P_FL_ENC_RAW, zip_fl_enc_raw),
+      (P_FL_ENC_GUESS, zip_fl_enc_guess),
+      (P_FL_ENC_STRICT, zip_fl_enc_strict)
+    ]
+
+-- | Gets the path (e.g., @"foo.txt"@, @"foo/"@, or @"foo/bar.txt"@) of the file at the given
+-- 0-based index in the given zip archive. Please use 'getNumEntries' to find the upper bound for
+-- the index.
+getPathByIndex :: Zip -> Int -> [PathFlag] -> IO ByteString
+getPathByIndex (Zip zipfp) idx flags =
+  let flags' = combineFlags pathFlags flags
+   in withForeignPtr zipfp $ \zipp -> do
+        name <- c_zip_get_name zipp (fromIntegral idx) flags'
+        if name == nullPtr
+          then do
+            err <- BC.unpack <$> (B.packCString =<< c_zip_strerror zipp)
+            throwError "getPathByIndex" err
+          else B.packCString name
+
+-- | A file inside of a 'Zip' archive.
+--
+-- /Internal/.
+data File
+  = File
+      !(Ptr Zip_file_t) -- ForeignPtr does not work because zip_fclose() does not return void.
+      !IOFinalizer
+
+data GetFileFlag
+  = -- | Read the compressed data. Otherwise the data is uncompressed when reading.
+    GF_FL_COMPRESSED
+  -- -- | Read the original data from the zip archive, ignoring any changes made to the file.
+  --  GF_FL_UNCHANGED
+  deriving (Eq, Ord)
+
+-- /Internal/.
+getFileFlags :: Map GetFileFlag Zip_flags_t
+getFileFlags =
+  M.fromList
+    [ (GF_FL_COMPRESSED, zip_fl_compressed)
+    -- (GF_FL_UNCHANGED, zip_fl_unchanged)
+    ]
+
+-- | We don't publicly expose getting a 'File' (and then unfolding from it) because we don't want
+-- users to unfold from the same 'File' more than once. (libzip’s @c_zip_fread@ isn’t designed for
+-- iterating through a file more than once.)
+--
+-- /Internal/.
+getFileByPathOrIndex :: Zip -> [GetFileFlag] -> Either String Int -> IO File
+getFileByPathOrIndex (Zip zipfp) flags pathOrIdx = mask_ $ do
+  let flags' = combineFlags getFileFlags flags
+  filep <- case pathOrIdx of
+    Left path ->
+      withCString path $ \pathc -> withForeignPtr zipfp $ \zipp -> do
+        filep <- c_zip_fopen zipp pathc flags'
+        when (filep == nullPtr) $ do
+          err <- BC.unpack <$> (B.packCString =<< c_zip_strerror zipp)
+          -- Keep zip alive (at least) until we have gotten the above error. This should take care
+          -- of touchForeignPtr’s “divergence” caveat. Ref (**).
+          touchForeignPtr zipfp
+          throwError "Error opening file at path" err
+        return filep
+    Right idx ->
+      withForeignPtr zipfp $ \zipp -> do
+        filep <- c_zip_fopen_index zipp (fromIntegral idx) flags'
+        when (filep == nullPtr) $ do
+          err <- BC.unpack <$> (B.packCString =<< c_zip_strerror zipp)
+          touchForeignPtr zipfp -- See (**).
+          throwError "Error opening file at index" err
+        return filep
+  ref <- newIOFinalizer $ do
+    ret <- c_zip_fclose filep
+    touchForeignPtr zipfp -- See (**).
+    when (ret /= 0) $
+      throwError
+        "Error closing file"
+        (printf "zip_fclose() return code: %d" (fromIntegral @_ @Int ret))
+  return $ File filep ref
+
+-- /Internal/.
+{-# INLINE unfoldFile #-}
+unfoldFile :: (MonadIO m) => Unfold m (Zip, [GetFileFlag], Either String Int) ByteString
+unfoldFile =
+  U.mkUnfoldM
+    ( \(z@(Zip zipfp), file@(File filep _), bufp, ref) -> liftIO $ do
+        bytesRead <- c_zip_fread filep bufp chunkSize
+        if bytesRead < 0
+          then
+            throwError
+              "Error reading file"
+              (printf "zip_fread() return value: %d" (fromIntegral @_ @Int bytesRead))
+          else
+            if bytesRead == 0
+              then do
+                runIOFinalizer ref
+                -- Keep zip alive for (at least) the duration of the unfold. See also (**).
+                touchForeignPtr zipfp
+                return U.Stop
+              else do
+                bs <- B.packCStringLen (bufp, fromIntegral bytesRead)
+                return $ U.Yield bs (z, file, bufp, ref)
+    )
+    ( \(z@(Zip zipfp), flags, pathOrIndex) -> liftIO $ mask_ $ do
+        file@(File _ fileFinalizer) <- getFileByPathOrIndex z flags pathOrIndex
+        bufp <- mallocBytes $ fromIntegral chunkSize
+        ref <- newIOFinalizer $ do
+          free bufp
+          runIOFinalizer fileFinalizer
+          touchForeignPtr zipfp -- See also (**).
+        return (z, file, bufp, ref)
+    )
+
+-- /Internal/.
+{-# INLINE chunkSize #-}
+chunkSize :: Zip_uint64_t
+chunkSize = 64000
+
+-- /Internal/.
+combineFlags :: (Ord flagType, Bits a, Num a) => Map flagType a -> [flagType] -> a
+combineFlags allFlags =
+  foldl'
+    (\acc chosenFlag -> acc .|. fromMaybe (error "flag expected") (M.lookup chosenFlag allFlags))
+    0
+
+-- | Creates an @Unfold@ with which one can stream data out of the entry at the given path (e.g.,
+-- @"foo.txt"@, @"foo/"@, or @"foo/bar.txt"@).
+unfoldFileAtPath :: (MonadIO m) => Unfold m (Zip, [GetFileFlag], String) ByteString
+unfoldFileAtPath = U.lmap (\(z, fl, p) -> (z, fl, Left p)) unfoldFile
+
+-- | Creates an @Unfold@ with which one can stream data out of the entry at the given index. Please
+-- use 'getNumEntries' to find the upper bound for the index.
+unfoldFileAtIndex :: (MonadIO m) => Unfold m (Zip, [GetFileFlag], Int) ByteString
+unfoldFileAtIndex = U.lmap (\(z, fl, idx) -> (z, fl, Right idx)) unfoldFile
diff --git a/src/Streamly/External/Zip/Internal/Error.hs b/src/Streamly/External/Zip/Internal/Error.hs
new file mode 100644
--- /dev/null
+++ b/src/Streamly/External/Zip/Internal/Error.hs
@@ -0,0 +1,26 @@
+module Streamly.External.Zip.Internal.Error where
+
+import Control.Exception
+import qualified Data.ByteString.Char8 as BC
+import Foreign.C.Types
+import Foreign.Marshal
+import Streamly.External.Zip.Internal.Foreign
+import Text.Printf
+
+data Error = Error !String !String
+
+instance Show Error where
+  show (Error ctx msg) = printf "streamly-zip; %s; %s" ctx msg
+
+instance Exception Error
+
+throwError :: String -> String -> m a
+throwError ctx msg = throw $ Error ctx msg
+
+libzipErrToString :: CInt -> IO String
+libzipErrToString err = mask_ $ do
+  errp <- mallocBytes zip_error_t_size
+  c_zip_error_init_with_code errp err
+  str <- BC.unpack <$> (c_zip_error_strerror errp >>= BC.packCString)
+  c_zip_error_fini errp
+  return str
diff --git a/src/Streamly/External/Zip/Internal/Foreign.hsc b/src/Streamly/External/Zip/Internal/Foreign.hsc
new file mode 100644
--- /dev/null
+++ b/src/Streamly/External/Zip/Internal/Foreign.hsc
@@ -0,0 +1,84 @@
+module Streamly.External.Zip.Internal.Foreign where
+
+import Data.Int
+import Foreign
+import Foreign.C.String
+import Foreign.C.Types
+
+#include <zip.h>
+type Zip_flags_t = #type zip_flags_t
+type Zip_int64_t = #type zip_int64_t
+type Zip_uint64_t = #type zip_uint64_t
+
+zip_error_t_size :: Int
+zip_error_t_size = #size zip_error_t
+
+data Zip_t
+
+data Zip_file_t
+
+data Zip_error_t
+
+foreign import ccall safe "zip.h zip_open"
+  c_zip_open :: CString -> CInt -> Ptr CInt -> IO (Ptr Zip_t)
+
+foreign import ccall safe "zip.h &zip_discard"
+  c_zip_discard_ptr :: FunPtr (Ptr Zip_t -> IO ())
+
+-- As this library currently only does reading, we don’t export zip_close().
+
+foreign import ccall safe "zip.h zip_get_num_entries"
+  c_zip_get_num_entries :: Ptr Zip_t -> Zip_flags_t -> IO Zip_int64_t
+
+foreign import ccall safe "zip.h zip_get_name"
+  c_zip_get_name :: Ptr Zip_t -> Zip_uint64_t -> Zip_flags_t -> IO (Ptr CChar)
+
+foreign import ccall safe "zip.h zip_fopen"
+  c_zip_fopen :: Ptr Zip_t -> CString -> Zip_flags_t -> IO (Ptr Zip_file_t)
+
+foreign import ccall safe "zip.h zip_fopen_index"
+  c_zip_fopen_index :: Ptr Zip_t -> Zip_uint64_t -> Zip_flags_t -> IO (Ptr Zip_file_t)
+
+foreign import ccall safe "zip.h zip_fclose"
+  c_zip_fclose :: Ptr Zip_file_t -> IO CInt
+
+foreign import ccall safe "zip.h zip_fread"
+  c_zip_fread :: Ptr Zip_file_t -> Ptr CChar -> Zip_uint64_t -> IO Zip_int64_t
+
+foreign import ccall safe "zip.h zip_strerror"
+  c_zip_strerror :: Ptr Zip_t -> IO (Ptr CChar)
+
+-- foreign import ccall safe "zip.h zip_file_strerror"
+--   c_zip_file_strerror :: Ptr Zip_file_t -> IO (Ptr CChar)
+
+foreign import ccall safe "zip.h zip_error_init_with_code"
+  c_zip_error_init_with_code :: Ptr Zip_error_t -> CInt -> IO ()
+
+foreign import ccall safe "zip.h zip_error_strerror"
+  c_zip_error_strerror :: Ptr Zip_error_t -> IO (Ptr CChar)
+
+foreign import ccall safe "zip.h zip_error_fini"
+  c_zip_error_fini :: Ptr Zip_error_t -> IO ()
+
+-- All flags relevant for the libzip functions we use.
+zip_checkcons,
+  zip_create,
+  zip_excl,
+  zip_truncate,
+  zip_rdonly,
+  zip_fl_compressed,
+  zip_fl_unchanged,
+  zip_fl_enc_raw,
+  zip_fl_enc_guess,
+  zip_fl_enc_strict ::
+    (Num a) => a
+zip_checkcons = #const ZIP_CHECKCONS
+zip_create = #const ZIP_CREATE
+zip_excl = #const ZIP_EXCL
+zip_truncate = #const ZIP_TRUNCATE
+zip_rdonly = #const ZIP_RDONLY
+zip_fl_compressed = #const ZIP_FL_COMPRESSED
+zip_fl_unchanged = #const ZIP_FL_UNCHANGED
+zip_fl_enc_raw = #const ZIP_FL_ENC_RAW
+zip_fl_enc_guess = #const ZIP_FL_ENC_GUESS
+zip_fl_enc_strict = #const ZIP_FL_ENC_STRICT
diff --git a/streamly-zip.cabal b/streamly-zip.cabal
new file mode 100644
--- /dev/null
+++ b/streamly-zip.cabal
@@ -0,0 +1,80 @@
+cabal-version:    3.0
+name:             streamly-zip
+version:          0.0.1
+synopsis:         Stream data from zip archives using the streamly library.
+description:      Please see the README on GitHub at <https://github.com/shlok/streamly-zip#readme>
+category:         Archive, Codec, Streaming, Streamly
+homepage:         https://github.com/shlok/streamly-zip
+bug-reports:      https://github.com/shlok/streamly-zip/issues
+author:           Shlok Datye
+maintainer:       sd-haskell@quant.is
+copyright:        2024 Shlok Datye
+license:          BSD-3-Clause
+license-file:     LICENSE
+build-type:       Simple
+extra-doc-files:
+  ChangeLog.md
+  README.md
+data-files:
+  test/data/data.zip
+
+source-repository head
+  type: git
+  location: https://github.com/shlok/streamly-zip
+
+library
+  exposed-modules:
+      Streamly.External.Zip
+      Streamly.External.Zip.Internal
+      Streamly.External.Zip.Internal.Error
+      Streamly.External.Zip.Internal.Foreign
+  other-modules:
+      Paths_streamly_zip
+  autogen-modules:
+      Paths_streamly_zip
+  hs-source-dirs:
+      src
+  ghc-options: -Wall
+  extra-libraries:
+      zip
+  build-depends:
+      base >= 4.14 && < 5
+    , bytestring >= 0.10 && < 0.12
+    , containers >=0.6 && <0.7
+    , streamly >=0.10.0 && <0.11
+    , streamly-core >=0.2.0 && <0.3
+  default-language: Haskell2010
+
+test-suite streamly-zip-test
+  type: exitcode-stdio-1.0
+  main-is: TestSuite.hs
+  other-modules:
+      ReadmeMain
+      Streamly.External.Zip.Tests
+      Paths_streamly_zip
+  autogen-modules:
+      Paths_streamly_zip
+  hs-source-dirs:
+      test
+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
+  extra-libraries:
+      zip
+  build-depends: 
+      base >= 4.14 && <5
+    , base16-bytestring >=1.0.1.0 && <1.1
+    , bytestring >= 0.10 && < 0.12
+    , containers >=0.6 && <0.7
+    , cryptohash >=0.11.9 && <0.12
+    , directory >=1.3.6.0 && <1.4
+    , filepath >=1.4.2.1 && <1.5
+    , QuickCheck >=2.13.2 && <2.15
+    , streamly >=0.10.0 && <0.11
+    , streamly-core >=0.2.0 && <0.3
+    , streamly-zip
+    , tar >=0.5.1.1 && <0.6
+    , tasty >=1.2.3 && <1.5
+    , tasty-hunit >=0.10.0.2 && <0.11
+    , tasty-quickcheck >=0.10.1.1 && <0.11
+    , temporary >=1.3 && <1.4
+    , zlib >=0.6.2.1 && <0.7
+  default-language: Haskell2010
diff --git a/test/ReadmeMain.hs b/test/ReadmeMain.hs
new file mode 100644
--- /dev/null
+++ b/test/ReadmeMain.hs
@@ -0,0 +1,17 @@
+module ReadmeMain where
+
+import qualified Data.ByteString as B
+import Data.Function
+import qualified Streamly.Data.Fold as F
+import qualified Streamly.Data.Stream.Prelude as S
+import Streamly.External.Zip
+
+main :: IO ()
+main = do
+  -- Obtain an archive.
+  z <- openZip "/path/to/archive.zip" []
+
+  -- Output a particular file to stdout.
+  S.unfold unfoldFileAtPath (z, [], "file.txt")
+    & S.mapM B.putStr
+    & S.fold F.drain
diff --git a/test/Streamly/External/Zip/Tests.hs b/test/Streamly/External/Zip/Tests.hs
new file mode 100644
--- /dev/null
+++ b/test/Streamly/External/Zip/Tests.hs
@@ -0,0 +1,51 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Streamly.External.Zip.Tests (tests) where
+
+import Control.Monad
+import qualified Crypto.Hash.SHA256 as SHA256
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Base16 as Base16
+import qualified Data.ByteString.Char8 as BC8
+import Data.Function
+import qualified Streamly.Data.Fold as F
+import qualified Streamly.Data.Stream.Prelude as S
+import Streamly.External.Zip
+import Test.Tasty
+import Test.Tasty.HUnit
+
+tests :: [TestTree]
+tests =
+  [testDataZip]
+
+testDataZip :: TestTree
+testDataZip = testCase "testDataZip" $ do
+  z <- openZip "test/data/data.zip" []
+
+  numEntries <- getNumEntries z
+  assertEqual "" numEntries 4
+
+  paths <- forM [0 .. numEntries - 1] $ \idx -> getPathByIndex z idx []
+  assertEqual "" paths ["1byte", "60kilobytes", "larger/", "larger/1megabyte"]
+  let indexedPaths = zip [0 ..] paths
+
+  let fol = S.fold (F.foldl' B.append B.empty)
+      pathToBs p = S.unfold unfoldFileAtPath (z, [], p) & fol
+      idxToBs idx = S.unfold unfoldFileAtIndex (z, [], idx) & fol
+
+  fileBytestrings2 <- forM indexedPaths $ \(idx, path) -> do
+    (,) <$> pathToBs (BC8.unpack path) <*> idxToBs idx
+
+  let fileBytestrings = map fst fileBytestrings2
+  assertEqual "" fileBytestrings (map snd fileBytestrings2)
+
+  let hashes = map (Base16.encode . SHA256.hash) fileBytestrings
+      expectedHashes =
+        [ "2d3193691934124461809fb9bc7e671215099fc7d961bfbe31943d40d477c890",
+          "8ef26f35dd26d5852a145e8ed64dad4db69820556fb267c23d76a513deaaaa80",
+          -- SHA256 of empty string.
+          "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+          "b9a25f19d36f7e39ceff25e1463eeaa9800a8fae12551d46f826ae87ccba4a40"
+        ]
+
+  assertEqual "" hashes expectedHashes
diff --git a/test/TestSuite.hs b/test/TestSuite.hs
new file mode 100644
--- /dev/null
+++ b/test/TestSuite.hs
@@ -0,0 +1,16 @@
+module Main where
+
+import qualified Streamly.External.Zip.Tests
+import Test.Tasty
+
+main :: IO ()
+main = defaultMain tests
+
+tests :: TestTree
+tests =
+  testGroup
+    "Tests"
+    [ testGroup
+        "Streamly.External.Zip.Tests"
+        Streamly.External.Zip.Tests.tests
+    ]
diff --git a/test/data/data.zip b/test/data/data.zip
new file mode 100644
Binary files /dev/null and b/test/data/data.zip differ
