streamly-zip (empty) → 0.0.1
raw patch · 13 files changed
+611/−0 lines, 13 filesdep +QuickCheckdep +basedep +base16-bytestringsetup-changedbinary-added
Dependencies added: QuickCheck, base, base16-bytestring, bytestring, containers, cryptohash, directory, filepath, streamly, streamly-core, streamly-zip, tar, tasty, tasty-hunit, tasty-quickcheck, temporary, zlib
Files
- ChangeLog.md +3/−0
- LICENSE +30/−0
- README.md +36/−0
- Setup.hs +2/−0
- src/Streamly/External/Zip.hs +23/−0
- src/Streamly/External/Zip/Internal.hs +243/−0
- src/Streamly/External/Zip/Internal/Error.hs +26/−0
- src/Streamly/External/Zip/Internal/Foreign.hsc +84/−0
- streamly-zip.cabal +80/−0
- test/ReadmeMain.hs +17/−0
- test/Streamly/External/Zip/Tests.hs +51/−0
- test/TestSuite.hs +16/−0
- test/data/data.zip binary
+ ChangeLog.md view
@@ -0,0 +1,3 @@+## 0.0.1++* Initial release.
+ LICENSE view
@@ -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.
+ README.md view
@@ -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+```
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ src/Streamly/External/Zip.hs view
@@ -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
+ src/Streamly/External/Zip/Internal.hs view
@@ -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
+ src/Streamly/External/Zip/Internal/Error.hs view
@@ -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
+ src/Streamly/External/Zip/Internal/Foreign.hsc view
@@ -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
+ streamly-zip.cabal view
@@ -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
+ test/ReadmeMain.hs view
@@ -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
+ test/Streamly/External/Zip/Tests.hs view
@@ -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
+ test/TestSuite.hs view
@@ -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+ ]
+ test/data/data.zip view
binary file changed (absent → 1060549 bytes)