zip 1.7.2 → 2.2.2
raw patch · 10 files changed
Files
- CHANGELOG.md +36/−0
- Codec/Archive/Zip.hs +13/−13
- Codec/Archive/Zip/CP437.hs +2/−2
- Codec/Archive/Zip/Internal.hs +130/−97
- Codec/Archive/Zip/Internal/Type.hs +230/−0
- Codec/Archive/Zip/Type.hs +0/−250
- README.md +4/−6
- Setup.hs +0/−6
- tests/Main.hs +129/−64
- zip.cabal +37/−38
CHANGELOG.md view
@@ -1,3 +1,39 @@+## Zip 2.2.2++* Make `sourceEntry` close file handles promptly. [Issue+ 142](https://github.com/mrkkrp/zip/issues/142).++## Zip 2.2.1++* Don't add Zip64 fields when copying entries from another archive+ unless the source entry has them. [PR+ 128](https://github.com/mrkkrp/zip/pull/128).++## Zip 2.2.0++* Skip the addition of Zip64 extra fields when sufficiently short strict+ input is provided (e.g. via `addEntry`). [Issue+ 126](https://github.com/mrkkrp/zip/issues/126).++## Zip 2.1.0++* Exposed `Codec.Archive.Zip.Internal` and `Codec.Archive.Zip.Internal.Type`+ modules. [PR 115](https://github.com/mrkkrp/zip/pull/115).++* Derived `Show` for `EntryDescription`. [PR+ 115](https://github.com/mrkkrp/zip/pull/115).++## Zip 2.0.1++* Fixed corruption of large entries when zip64 is used. [Issue+ 111](https://github.com/mrkkrp/zip/issues/111).++## Zip 2.0.0++* Unified `BZip2Unsupported` and `ZstdUnsupported` into a single data+ constructor `UnsupportedCompressionMethod` with a `CompressionMethod`+ field.+ ## Zip 1.7.2 * Now the ZIP64 extra field is only written when it is necessary. Previously
Codec/Archive/Zip.hs view
@@ -146,8 +146,8 @@ ) where -import qualified Codec.Archive.Zip.Internal as I-import Codec.Archive.Zip.Type+import Codec.Archive.Zip.Internal qualified as I+import Codec.Archive.Zip.Internal.Type import Conduit (PrimMonad) import Control.Monad import Control.Monad.Base (MonadBase (..))@@ -157,22 +157,22 @@ import Control.Monad.Trans.Resource (MonadResource, ResourceT) import Data.ByteString (ByteString) import Data.Conduit (ConduitT, (.|))-import qualified Data.Conduit as C-import qualified Data.Conduit.Binary as CB-import qualified Data.Conduit.List as CL-import qualified Data.DList as DList+import Data.Conduit qualified as C+import Data.Conduit.Binary qualified as CB+import Data.Conduit.List qualified as CL+import Data.DList qualified as DList import Data.Map.Strict (Map, (!))-import qualified Data.Map.Strict as M+import Data.Map.Strict qualified as M import Data.Sequence (Seq, (|>))-import qualified Data.Sequence as S-import qualified Data.Set as E+import Data.Sequence qualified as S+import Data.Set qualified as E import Data.Text (Text) import Data.Time.Clock (UTCTime) import Data.Void import Data.Word (Word16, Word32) import System.Directory import System.FilePath ((</>))-import qualified System.FilePath as FP+import System.FilePath qualified as FP import System.IO.Error (isDoesNotExistError) #ifndef mingw32_HOST_OS@@ -233,7 +233,7 @@ -- specified file if it already exists. See 'withArchive' if you want to -- work with an existing archive. createArchive ::- MonadIO m =>+ (MonadIO m) => -- | Location of the archive file to create FilePath -> -- | Actions that create the archive's content@@ -277,7 +277,7 @@ -- design decision to make it impossible to create non-portable archives -- with this library. withArchive ::- MonadIO m =>+ (MonadIO m) => -- | Location of the archive to work with FilePath -> -- | Actions on that archive@@ -431,7 +431,7 @@ -- | Name of the entry to add EntrySelector -> ZipArchive ()-addEntry t b s = addPending (I.SinkEntry t (C.yield b) s)+addEntry t b s = addPending (I.StrictEntry t b s) -- | Stream data from the specified source to an archive entry. sinkEntry ::
Codec/Archive/Zip/CP437.hs view
@@ -15,10 +15,10 @@ import Control.Arrow (first) import Data.ByteString (ByteString)-import qualified Data.ByteString as B+import Data.ByteString qualified as B import Data.Char import Data.Text (Text)-import qualified Data.Text as T+import Data.Text qualified as T import Data.Word (Word8) -- | Decode a 'ByteString' containing CP 437 encoded text.
Codec/Archive/Zip/Internal.hs view
@@ -13,18 +13,10 @@ -- Portability : portable -- -- Low-level, non-public types and operations.-module Codec.Archive.Zip.Internal- ( PendingAction (..),- targetEntry,- scanArchive,- sourceEntry,- crc32Sink,- commit,- )-where+module Codec.Archive.Zip.Internal where import Codec.Archive.Zip.CP437 (decodeCP437)-import Codec.Archive.Zip.Type+import Codec.Archive.Zip.Internal.Type import Conduit (PrimMonad) import Control.Applicative (many, (<|>)) import Control.Exception (bracketOnError, catchJust)@@ -35,26 +27,25 @@ import Data.Bits import Data.Bool (bool) import Data.ByteString (ByteString)-import qualified Data.ByteString as B+import Data.ByteString qualified as B import Data.Char (ord) import Data.Conduit (ConduitT, ZipSink (..), (.|))-import qualified Data.Conduit as C-import qualified Data.Conduit.Binary as CB-import qualified Data.Conduit.List as CL-import qualified Data.Conduit.Zlib as Z+import Data.Conduit qualified as C+import Data.Conduit.Binary qualified as CB+import Data.Conduit.List qualified as CL+import Data.Conduit.Zlib qualified as Z import Data.Digest.CRC32 (crc32Update) import Data.Fixed (Fixed (..))-import Data.Foldable (foldl')-import Data.Map.Strict (Map, (!))-import qualified Data.Map.Strict as M+import Data.Map.Strict (Map)+import Data.Map.Strict qualified as M import Data.Maybe (catMaybes, fromJust, isNothing) import Data.Sequence (Seq, (><), (|>))-import qualified Data.Sequence as S+import Data.Sequence qualified as S import Data.Serialize-import qualified Data.Set as E+import Data.Set qualified as E import Data.Text (Text)-import qualified Data.Text as T-import qualified Data.Text.Encoding as T+import Data.Text qualified as T+import Data.Text.Encoding qualified as T import Data.Time import Data.Version import Data.Void@@ -77,6 +68,10 @@ import qualified Data.Conduit.Zstd as Zstandard #endif +#if !MIN_VERSION_base(4,20,0)+import Data.Foldable (foldl')+#endif+ ---------------------------------------------------------------------------- -- Data types @@ -88,7 +83,9 @@ CompressionMethod (ConduitT () ByteString (ResourceT IO) ()) EntrySelector- | -- | Copy an entry form another archive without re-compression+ | -- | Add an entry given its content as a string ByteString.+ StrictEntry CompressionMethod ByteString EntrySelector+ | -- | Copy an entry from another archive without re-compression CopyEntry FilePath EntrySelector EntrySelector | -- | Change the name of the entry inside archive RenameEntry EntrySelector EntrySelector@@ -117,7 +114,7 @@ -- archive. data ProducingActions = ProducingActions { paCopyEntry :: Map FilePath (Map EntrySelector EntrySelector),- paSinkEntry :: Map EntrySelector (ConduitT () ByteString (ResourceT IO) ())+ paSinkEntry :: Map EntrySelector (EntryOrigin, ConduitT () ByteString (ResourceT IO) ()) } -- | A collection of editing actions, that is, actions that modify already@@ -135,11 +132,13 @@ -- | The origin of entries that can be streamed into archive. data EntryOrigin = GenericOrigin+ | StrictOrigin Natural -- uncompressed length | Borrowed EntryDescription+ deriving (Eq) -- | The type of the file header: local or central directory. data HeaderType- = LocalHeader+ = LocalHeader EntryOrigin | CentralDirHeader deriving (Eq) @@ -239,18 +238,29 @@ Bool -> -- | Source of uncompressed data ConduitT () ByteString m ()-sourceEntry path EntryDescription {..} d =- source .| CB.isolate (fromIntegral edCompressedSize) .| decompress+sourceEntry path (EntryDescription {..}) d =+ -- We cannot simply use CB.sourceIOHandle+ -- because that closes the archive only when its end is reached.+ -- See <https://github.com/mrkkrp/zip/issues/142>.+ -- The following implementation closes file handles promptly.+ C.bracketP+ ( do+ h <- openBinaryFile path ReadMode+ hSeek h AbsoluteSeek (fromIntegral edOffset)+ localHeader <- B.hGet h 30+ case runGet getLocalHeaderGap localHeader of+ Left msg -> throwM (ParsingFailed path msg)+ Right gap -> do+ hSeek h RelativeSeek gap+ return h+ )+ hClose+ ( \h ->+ CB.sourceHandle h+ .| CB.isolate (fromIntegral edCompressedSize)+ .| decompress+ ) where- source = CB.sourceIOHandle $ do- h <- openFile path ReadMode- hSeek h AbsoluteSeek (fromIntegral edOffset)- localHeader <- B.hGet h 30- case runGet getLocalHeaderGap localHeader of- Left msg -> throwM (ParsingFailed path msg)- Right gap -> do- hSeek h RelativeSeek gap- return h decompress = if d then decompressingPipe edCompression@@ -265,31 +275,29 @@ FilePath -> -- | Archive description ArchiveDescription ->- -- | Current list of entires+ -- | Current list of entries Map EntrySelector EntryDescription -> -- | Collection of pending actions Seq PendingAction -> IO () commit path ArchiveDescription {..} entries xs = withNewFile path $ \h -> do- let (ProducingActions coping sinking, editing) =+ let (ProducingActions copying sinking, editing) = optimize (toRecreatingActions path entries >< xs) comment = predictComment adComment xs copiedCD <- M.unions- <$> forM- (M.keys coping)- ( \srcPath ->- copyEntries h srcPath (coping ! srcPath) editing+ <$> M.traverseWithKey+ ( \srcPath m ->+ copyEntries h srcPath m editing )- let sinkingKeys = M.keys $ sinking `M.difference` copiedCD+ copying sunkCD <-- M.fromList- <$> forM- sinkingKeys- ( \selector ->- sinkEntry h selector GenericOrigin (sinking ! selector) editing- )+ M.traverseWithKey+ ( \selector (origin, source) ->+ sinkEntry h selector origin source editing+ )+ (sinking `M.difference` copiedCD) writeCD h comment (copiedCD `M.union` sunkCD) -- | Create a new file with the guarantee that in the case of an exception@@ -356,13 +364,24 @@ f (pa, ea) a = case a of SinkEntry m src s -> ( pa- { paSinkEntry = M.insert s src (paSinkEntry pa),+ { paSinkEntry = M.insert s (GenericOrigin, src) (paSinkEntry pa), paCopyEntry = M.map (M.filter (/= s)) (paCopyEntry pa) }, (clearEditingFor s ea) { eaCompression = M.insert s m (eaCompression ea) } )+ StrictEntry m bs s ->+ ( pa+ { paSinkEntry = M.insert s (origin, C.yield bs) (paSinkEntry pa),+ paCopyEntry = M.map (M.filter (/= s)) (paCopyEntry pa)+ },+ (clearEditingFor s ea)+ { eaCompression = M.insert s m (eaCompression ea)+ }+ )+ where+ origin = StrictOrigin (fromIntegral $ B.length bs) CopyEntry path os ns -> ( pa { paSinkEntry = M.delete ns (paSinkEntry pa),@@ -460,19 +479,21 @@ EditingActions -> -- | Info to generate central directory file headers later IO (Map EntrySelector EntryDescription)-copyEntries h path m e = do+copyEntries h path names editing = do entries <- snd <$> scanArchive path- done <- forM (M.keys m) $ \s ->- case s `M.lookup` entries of- Nothing -> throwM (EntryDoesNotExist path s)- Just desc ->- sinkEntry- h- (m ! s)- (Borrowed desc)- (sourceEntry path desc False)- e- return (M.fromList done)+ M.foldlWithKey+ ( \acc oldName newName ->+ case M.lookup oldName entries of+ Nothing -> throwM (EntryDoesNotExist path oldName)+ Just oldDesc ->+ M.insert newName+ <$> sinkEntry h newName (Borrowed oldDesc) src editing+ <*> acc+ where+ src = sourceEntry path oldDesc False+ )+ mempty+ names -- | Sink an entry from the given stream into the file associated with the -- given 'Handle'.@@ -488,31 +509,28 @@ -- | Additional info that can influence result EditingActions -> -- | Info to generate the central directory file headers later- IO (EntrySelector, EntryDescription)+ IO EntryDescription sinkEntry h s o src EditingActions {..} = do currentTime <- getCurrentTime offset <- hTell h let compressed = case o of- GenericOrigin -> Store Borrowed ed -> edCompression ed+ _ -> Store compression = M.findWithDefault compressed s eaCompression recompression = compression /= compressed modTime = case o of- GenericOrigin -> currentTime Borrowed ed -> edModTime ed- extFileAttr = case o of- GenericOrigin -> M.findWithDefault defaultFileMode s eaExtFileAttr- Borrowed _ -> M.findWithDefault defaultFileMode s eaExtFileAttr+ _ -> currentTime+ extFileAttr = M.findWithDefault defaultFileMode s eaExtFileAttr oldExtraFields = case o of- GenericOrigin -> M.empty Borrowed ed -> edExtraField ed+ _ -> M.empty extraField = (M.findWithDefault M.empty s eaExtraField `M.union` oldExtraFields) `M.difference` M.findWithDefault M.empty s eaDeleteField oldComment = case (o, M.lookup s eaDeleteComment) of- (GenericOrigin, _) -> Nothing (Borrowed ed, Nothing) -> edComment ed- (Borrowed _, Just ()) -> Nothing+ _ -> Nothing desc0 = EntryDescription -- to write in local header { edVersionMadeBy = zipVersion,@@ -527,7 +545,7 @@ edExtraField = extraField, edExternalFileAttrs = extFileAttr }- B.hPut h (runPut (putHeader LocalHeader s desc0))+ B.hPut h (runPut (putHeader (LocalHeader o) s desc0)) DataDescriptor {..} <- C.runConduitRes $ if recompression@@ -538,12 +556,6 @@ else src .| sinkData h Store afterStreaming <- hTell h let desc1 = case o of- GenericOrigin ->- desc0- { edCRC32 = ddCRC32,- edCompressedSize = ddCompressedSize,- edUncompressedSize = ddUncompressedSize- } Borrowed ed -> desc0 { edCRC32 =@@ -553,15 +565,21 @@ edUncompressedSize = bool (edUncompressedSize ed) ddUncompressedSize recompression }+ _ ->+ desc0+ { edCRC32 = ddCRC32,+ edCompressedSize = ddCompressedSize,+ edUncompressedSize = ddUncompressedSize+ } desc2 = desc1 { edVersionNeeded = getZipVersion (needsZip64 desc1) (Just compression) } hSeek h AbsoluteSeek offset- B.hPut h (runPut (putHeader LocalHeader s desc2))+ B.hPut h (runPut (putHeader (LocalHeader o) s desc2)) hSeek h AbsoluteSeek afterStreaming- return (s, desc2)+ return desc2 {- ORMOLU_DISABLE -} @@ -598,14 +616,14 @@ withCompression $ BZ.bzip2 .| dataSink #else- BZip2 -> throwM BZip2Unsupported+ BZip2 -> throwM (UnsupportedCompressionMethod BZip2) #endif #ifdef ENABLE_ZSTD Zstd -> withCompression $ Zstandard.compress 1 .| dataSink #else- Zstd -> throwM ZstdUnsupported+ Zstd -> throwM (UnsupportedCompressionMethod Zstd) #endif return DataDescriptor@@ -779,25 +797,29 @@ -- | Resulting representation ByteString makeZip64ExtraField headerType Zip64ExtraField {..} = runPut $ do- when (headerType == LocalHeader || z64efUncompressedSize >= ffffffff) $- putWord64le (fromIntegral z64efUncompressedSize) -- uncompressed size- when (headerType == LocalHeader || z64efCompressedSize >= ffffffff) $- putWord64le (fromIntegral z64efCompressedSize) -- compressed size- when (headerType == CentralDirHeader && z64efOffset >= ffffffff) $+ case headerType of+ LocalHeader _ -> do+ putWord64le (fromIntegral z64efUncompressedSize) -- uncompressed size+ putWord64le (fromIntegral z64efCompressedSize) -- compressed size+ CentralDirHeader -> do+ when (z64efUncompressedSize >= ffffffff) $+ putWord64le (fromIntegral z64efUncompressedSize) -- uncompressed size+ when (z64efCompressedSize >= ffffffff) $+ putWord64le (fromIntegral z64efCompressedSize) -- compressed size+ when (z64efOffset >= ffffffff) $ putWord64le (fromIntegral z64efOffset) -- offset of local file header -- | Create 'ByteString' representing an extra field. putExtraField :: Map Word16 ByteString -> Put-putExtraField m = forM_ (M.keys m) $ \headerId -> do- let b = B.take 0xffff (m ! headerId)+putExtraField = M.foldMapWithKey $ \headerId bs -> do+ let b = B.take 0xffff bs putWord16le headerId putWord16le (fromIntegral $ B.length b) putByteString b -- | Create 'ByteString' representing the entire central directory. putCD :: Map EntrySelector EntryDescription -> Put-putCD m = forM_ (M.keys m) $ \s ->- putHeader CentralDirHeader s (m ! s)+putCD = M.foldMapWithKey (putHeader CentralDirHeader) -- | Create 'ByteString' representing either a local file header or a -- central directory file header.@@ -840,9 +862,15 @@ z64efCompressedSize = edCompressedSize, z64efOffset = edOffset }+ appendZip64 =+ case headerType of+ LocalHeader (StrictOrigin size) -> size >= ffffffff+ LocalHeader (Borrowed ed) -> entryUsesZip64 ed+ LocalHeader GenericOrigin -> True+ CentralDirHeader -> needsZip64 entry extraField = B.take 0xffff . runPut . putExtraField $- if needsZip64 entry+ if appendZip64 then M.insert 1 zip64ef edExtraField else edExtraField putWord16le (fromIntegral $ B.length extraField) -- extra field length@@ -1033,7 +1061,7 @@ -- Helpers -- | Rename an entry (key) in a 'Map'.-renameKey :: Ord k => k -> k -> Map k a -> Map k a+renameKey :: (Ord k) => k -> k -> Map k a -> Map k a renameKey ok nk m = case M.lookup ok m of Nothing -> m Just e -> M.insert nk e (M.delete ok m)@@ -1051,6 +1079,7 @@ -- | Determine the target entry of an action. targetEntry :: PendingAction -> Maybe EntrySelector targetEntry (SinkEntry _ _ s) = Just s+targetEntry (StrictEntry _ _ s) = Just s targetEntry (CopyEntry _ _ s) = Just s targetEntry (RenameEntry s _) = Just s targetEntry (DeleteEntry s) = Just s@@ -1127,6 +1156,10 @@ (>= ffffffff) [edOffset, edCompressedSize, edUncompressedSize] +-- | Check if an entry has Zip64 extra fields.+entryUsesZip64 :: EntryDescription -> Bool+entryUsesZip64 EntryDescription {..} = M.member 1 edExtraField+ -- | Determine “version needed to extract” that should be written to the -- headers given the need of the Zip64 feature and the compression method. getZipVersion :: Bool -> Maybe CompressionMethod -> Version@@ -1152,13 +1185,13 @@ #ifdef ENABLE_BZIP2 decompressingPipe BZip2 = BZ.bunzip2 #else-decompressingPipe BZip2 = throwM BZip2Unsupported+decompressingPipe BZip2 = throwM (UnsupportedCompressionMethod BZip2) #endif #ifdef ENABLE_ZSTD decompressingPipe Zstd = Zstandard.decompress #else-decompressingPipe Zstd = throwM ZstdUnsupported+decompressingPipe Zstd = throwM (UnsupportedCompressionMethod Zstd) #endif -- | A sink that calculates the CRC32 check sum for an incoming stream.@@ -1204,8 +1237,8 @@ ffff, ffffffff :: Natural #ifdef HASKELL_ZIP_DEV_MODE-ffff = 200-ffffffff = 5000+ffff = 25+ffffffff = 250 #else ffff = 0xffff ffffffff = 0xffffffff
+ Codec/Archive/Zip/Internal/Type.hs view
@@ -0,0 +1,230 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveDataTypeable #-}++-- |+-- Module : Codec.Archive.Zip.Internal.Type+-- Copyright : © 2016–present Mark Karpov+-- License : BSD 3 clause+--+-- Maintainer : Mark Karpov <markkarpov92@gmail.com>+-- Stability : experimental+-- Portability : portable+--+-- Types used by the package.+module Codec.Archive.Zip.Internal.Type+ ( -- * Entry selector+ EntrySelector,+ mkEntrySelector,+ unEntrySelector,+ getEntryName,+ EntrySelectorException (..),++ -- * Entry description+ EntryDescription (..),+ CompressionMethod (..),++ -- * Archive description+ ArchiveDescription (..),++ -- * Exceptions+ ZipException (..),+ )+where++import Control.Exception (Exception)+import Control.Monad.Catch (MonadThrow (..))+import Data.ByteString (ByteString)+import Data.ByteString qualified as B+import Data.CaseInsensitive (CI)+import Data.CaseInsensitive qualified as CI+import Data.Data (Data)+import Data.List.NonEmpty (NonEmpty)+import Data.List.NonEmpty qualified as NE+import Data.Map (Map)+import Data.Maybe (mapMaybe)+import Data.Text (Text)+import Data.Text qualified as T+import Data.Text.Encoding qualified as T+import Data.Time.Clock (UTCTime)+import Data.Version (Version)+import Data.Word (Word16, Word32)+import Numeric.Natural+import System.FilePath qualified as FP+import System.FilePath.Posix qualified as Posix+import System.FilePath.Windows qualified as Windows++----------------------------------------------------------------------------+-- Entry selector++-- | This data type serves for naming and selection of archive entries. It+-- can be created only with the help of the smart constructor+-- 'mkEntrySelector', and it's the only “key” that can be used to refer to+-- files in the archive or to name new archive entries.+--+-- The abstraction is crucial for ensuring that created archives are+-- portable across operating systems, file systems, and platforms. Since on+-- some operating systems, file paths are case-insensitive, this selector is+-- also case-insensitive. It makes sure that only relative paths are used to+-- name files inside archive, as it's recommended in the specification. It+-- also guarantees that forward slashes are used when the path is stored+-- inside the archive for compatibility with Unix-like operating systems (as+-- recommended in the specification). On the other hand, in can be rendered+-- as an ordinary relative file path in OS-specific format when needed.+newtype EntrySelector = EntrySelector+ { -- | Path pieces of relative path inside archive+ unES :: NonEmpty (CI String)+ }+ deriving (Eq, Ord)++instance Show EntrySelector where+ show = show . unEntrySelector++-- | Create an 'EntrySelector' from a 'FilePath'. To avoid problems with+-- distribution of the archive, characters that some operating systems do+-- not expect in paths are not allowed.+--+-- Argument to 'mkEntrySelector' should pass these checks:+--+-- * 'System.FilePath.Posix.isValid'+-- * 'System.FilePath.Windows.isValid'+-- * it is a relative path without slash at the end+-- * binary representations of normalized path should be not longer than+-- 65535 bytes+--+-- This function can throw an 'EntrySelectorException'.+mkEntrySelector :: (MonadThrow m) => FilePath -> m EntrySelector+mkEntrySelector path =+ let f x =+ case filter (not . FP.isPathSeparator) x of+ [] -> Nothing+ xs -> Just (CI.mk xs)+ giveup = throwM (InvalidEntrySelector path)+ in case NE.nonEmpty (mapMaybe f (FP.splitPath path)) of+ Nothing -> giveup+ Just pieces ->+ let selector = EntrySelector pieces+ binLength = B.length . T.encodeUtf8 . getEntryName+ in if Posix.isValid path+ && Windows.isValid path+ && not (FP.isAbsolute path || FP.hasTrailingPathSeparator path)+ && (CI.mk "." `notElem` pieces)+ && (CI.mk ".." `notElem` pieces)+ && binLength selector <= 0xffff+ then return selector+ else giveup++-- | Restore a relative path from 'EntrySelector'. Every 'EntrySelector'+-- corresponds to a 'FilePath'.+unEntrySelector :: EntrySelector -> FilePath+unEntrySelector =+ FP.joinPath . fmap CI.original . NE.toList . unES++-- | Get an entry name in the from that is suitable for writing to file+-- header, given an 'EntrySelector'.+getEntryName :: EntrySelector -> Text+getEntryName =+ T.pack . concat . NE.toList . NE.intersperse "/" . fmap CI.original . unES++-- | The problems you can have with an 'EntrySelector'.+newtype EntrySelectorException+ = -- | 'EntrySelector' cannot be created from this path+ InvalidEntrySelector FilePath+ deriving (Eq, Ord)++instance Show EntrySelectorException where+ show (InvalidEntrySelector path) = "Cannot build selector from " ++ show path++instance Exception EntrySelectorException++----------------------------------------------------------------------------+-- Entry description++-- | The information about archive entry that can be stored in a zip+-- archive. It does not mirror local file header or central directory file+-- header, but their binary representations can be built given this data+-- structure and the archive contents.+data EntryDescription = EntryDescription+ { -- | Version made by+ edVersionMadeBy :: Version,+ -- | Version needed to extract+ edVersionNeeded :: Version,+ -- | Compression method+ edCompression :: CompressionMethod,+ -- | Last modification date and time+ edModTime :: UTCTime,+ -- | CRC32 check sum+ edCRC32 :: Word32,+ -- | Size of compressed entry+ edCompressedSize :: Natural,+ -- | Size of uncompressed entry+ edUncompressedSize :: Natural,+ -- | Absolute offset of local file header+ edOffset :: Natural,+ -- | Entry comment+ edComment :: Maybe Text,+ -- | All extra fields found+ edExtraField :: Map Word16 ByteString,+ -- | External file attributes+ --+ -- @since 1.2.0+ edExternalFileAttrs :: Word32+ }+ deriving (Eq, Show)++-- | The supported compression methods.+data CompressionMethod+ = -- | Store file uncompressed+ Store+ | -- | Deflate+ Deflate+ | -- | Compressed using BZip2 algorithm+ BZip2+ | -- | Compressed using Zstandard algorithm+ --+ -- @since 1.6.0+ Zstd+ deriving (Show, Read, Eq, Ord, Enum, Bounded, Data)++----------------------------------------------------------------------------+-- Archive description++-- | The information about the archive as a whole.+data ArchiveDescription = ArchiveDescription+ { -- | The comment of the entire archive+ adComment :: Maybe Text,+ -- | Absolute offset of the start of central directory+ adCDOffset :: Natural,+ -- | The size of central directory record+ adCDSize :: Natural+ }+ deriving (Show, Read, Eq, Ord, Data)++----------------------------------------------------------------------------+-- Exceptions++-- | The bad things that can happen when you use the library.+data ZipException+ = -- | Thrown when you try to get contents of non-existing entry+ EntryDoesNotExist FilePath EntrySelector+ | -- | Thrown when attempting to decompress an entry compressed with an+ -- unsupported compression method or the library is compiled without+ -- support for it.+ --+ -- @since 2.0.0+ UnsupportedCompressionMethod CompressionMethod+ | -- | Thrown when archive structure cannot be parsed.+ ParsingFailed FilePath String+ deriving (Eq, Ord)++instance Show ZipException where+ show (EntryDoesNotExist file s) =+ "No such entry found: " ++ show s ++ " in " ++ show file+ show (ParsingFailed file msg) =+ "Parsing of archive structure failed: \n" ++ msg ++ "\nin " ++ show file+ show (UnsupportedCompressionMethod method) =+ "Encountered a zipfile entry with "+ ++ show method+ ++ " compression, but "+ ++ "zip library does not support it or has been built without support for it."++instance Exception ZipException
− Codec/Archive/Zip/Type.hs
@@ -1,250 +0,0 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE DeriveDataTypeable #-}---- |--- Module : Codec.Archive.Zip.Type--- Copyright : © 2016–present Mark Karpov--- License : BSD 3 clause------ Maintainer : Mark Karpov <markkarpov92@gmail.com>--- Stability : experimental--- Portability : portable------ Types used by the package.-module Codec.Archive.Zip.Type- ( -- * Entry selector- EntrySelector,- mkEntrySelector,- unEntrySelector,- getEntryName,- EntrySelectorException (..),-- -- * Entry description- EntryDescription (..),- CompressionMethod (..),-- -- * Archive description- ArchiveDescription (..),-- -- * Exceptions- ZipException (..),- )-where--import Control.Exception (Exception)-import Control.Monad.Catch (MonadThrow (..))-import Data.ByteString (ByteString)-import qualified Data.ByteString as B-import Data.CaseInsensitive (CI)-import qualified Data.CaseInsensitive as CI-import Data.Data (Data)-import Data.List.NonEmpty (NonEmpty)-import qualified Data.List.NonEmpty as NE-import Data.Map (Map)-import Data.Maybe (mapMaybe)-import Data.Text (Text)-import qualified Data.Text as T-import qualified Data.Text.Encoding as T-import Data.Time.Clock (UTCTime)-import Data.Typeable (Typeable)-import Data.Version (Version)-import Data.Word (Word16, Word32)-import Numeric.Natural-import qualified System.FilePath as FP-import qualified System.FilePath.Posix as Posix-import qualified System.FilePath.Windows as Windows--------------------------------------------------------------------------------- Entry selector---- | This data type serves for naming and selection of archive entries. It--- can be created only with the help of the smart constructor--- 'mkEntrySelector', and it's the only “key” that can be used to refer to--- files in the archive or to name new archive entries.------ The abstraction is crucial for ensuring that created archives are--- portable across operating systems, file systems, and platforms. Since on--- some operating systems, file paths are case-insensitive, this selector is--- also case-insensitive. It makes sure that only relative paths are used to--- name files inside archive, as it's recommended in the specification. It--- also guarantees that forward slashes are used when the path is stored--- inside the archive for compatibility with Unix-like operating systems (as--- recommended in the specification). On the other hand, in can be rendered--- as an ordinary relative file path in OS-specific format when needed.-newtype EntrySelector = EntrySelector- { -- | Path pieces of relative path inside archive- unES :: NonEmpty (CI String)- }- deriving (Eq, Ord, Typeable)--instance Show EntrySelector where- show = show . unEntrySelector---- | Create an 'EntrySelector' from a 'FilePath'. To avoid problems with--- distribution of the archive, characters that some operating systems do--- not expect in paths are not allowed.------ Argument to 'mkEntrySelector' should pass these checks:------ * 'System.FilePath.Posix.isValid'--- * 'System.FilePath.Windows.isValid'--- * it is a relative path without slash at the end--- * binary representations of normalized path should be not longer than--- 65535 bytes------ This function can throw an 'EntrySelectorException'.-mkEntrySelector :: MonadThrow m => FilePath -> m EntrySelector-mkEntrySelector path =- let f x =- case filter (not . FP.isPathSeparator) x of- [] -> Nothing- xs -> Just (CI.mk xs)- giveup = throwM (InvalidEntrySelector path)- in case NE.nonEmpty (mapMaybe f (FP.splitPath path)) of- Nothing -> giveup- Just pieces ->- let selector = EntrySelector pieces- binLength = B.length . T.encodeUtf8 . getEntryName- in if Posix.isValid path- && Windows.isValid path- && not (FP.isAbsolute path || FP.hasTrailingPathSeparator path)- && (CI.mk "." `notElem` pieces)- && (CI.mk ".." `notElem` pieces)- && binLength selector <= 0xffff- then return selector- else giveup---- | Restore a relative path from 'EntrySelector'. Every 'EntrySelector'--- corresponds to a 'FilePath'.-unEntrySelector :: EntrySelector -> FilePath-unEntrySelector =- FP.joinPath . fmap CI.original . NE.toList . unES---- | Get an entry name in the from that is suitable for writing to file--- header, given an 'EntrySelector'.-getEntryName :: EntrySelector -> Text-getEntryName =- T.pack . concat . NE.toList . NE.intersperse "/" . fmap CI.original . unES---- | The problems you can have with an 'EntrySelector'.-newtype EntrySelectorException- = -- | 'EntrySelector' cannot be created from this path- InvalidEntrySelector FilePath- deriving (Eq, Ord, Typeable)--instance Show EntrySelectorException where- show (InvalidEntrySelector path) = "Cannot build selector from " ++ show path--instance Exception EntrySelectorException--------------------------------------------------------------------------------- Entry description---- | The information about archive entry that can be stored in a zip--- archive. It does not mirror local file header or central directory file--- header, but their binary representations can be built given this data--- structure and the archive contents.-data EntryDescription = EntryDescription- { -- | Version made by- edVersionMadeBy :: Version,- -- | Version needed to extract- edVersionNeeded :: Version,- -- | Compression method- edCompression :: CompressionMethod,- -- | Last modification date and time- edModTime :: UTCTime,- -- | CRC32 check sum- edCRC32 :: Word32,- -- | Size of compressed entry- edCompressedSize :: Natural,- -- | Size of uncompressed entry- edUncompressedSize :: Natural,- -- | Absolute offset of local file header- edOffset :: Natural,- -- | Entry comment- edComment :: Maybe Text,- -- | All extra fields found- edExtraField :: Map Word16 ByteString,- -- | External file attributes- --- -- @since 1.2.0- edExternalFileAttrs :: Word32- }- deriving (Eq, Typeable)---- | The supported compression methods.-data CompressionMethod- = -- | Store file uncompressed- Store- | -- | Deflate- Deflate- | -- | Compressed using BZip2 algorithm- BZip2- | -- | Compressed using Zstandard algorithm- --- -- @since 1.6.0- Zstd- deriving (Show, Read, Eq, Ord, Enum, Bounded, Data, Typeable)--------------------------------------------------------------------------------- Archive description---- | The information about the archive as a whole.-data ArchiveDescription = ArchiveDescription- { -- | The comment of the entire archive- adComment :: Maybe Text,- -- | Absolute offset of the start of central directory- adCDOffset :: Natural,- -- | The size of central directory record- adCDSize :: Natural- }- deriving (Show, Read, Eq, Ord, Typeable, Data)--------------------------------------------------------------------------------- Exceptions--{- ORMOLU_DISABLE -}---- | The bad things that can happen when you use the library.-data ZipException- = -- | Thrown when you try to get contents of non-existing entry- EntryDoesNotExist FilePath EntrySelector-#ifndef ENABLE_BZIP2- -- | Thrown when attempting to decompress a 'BZip2' entry and the- -- library is compiled without support for it.- --- -- @since 1.3.0- | BZip2Unsupported-#endif-#ifndef ENABLE_ZSTD- -- | Thrown when attempting to decompress a 'Zstd' entry and the- -- library is compiled without support for it.- --- -- @since 1.6.0- | ZstdUnsupported-#endif- -- | Thrown when archive structure cannot be parsed.- | ParsingFailed FilePath String- deriving (Eq, Ord, Typeable)--{- ORMOLU_ENABLE -}--instance Show ZipException where- show (EntryDoesNotExist file s) =- "No such entry found: " ++ show s ++ " in " ++ show file- show (ParsingFailed file msg) =- "Parsing of archive structure failed: \n" ++ msg ++ "\nin " ++ show file--#ifndef ENABLE_BZIP2- show BZip2Unsupported =- "Encountered a zipfile entry with BZip2 compression, but " ++- "the zip library has been built with bzip2 disabled."-#endif--#ifndef ENABLE_ZSTD- show ZstdUnsupported =- "Encountered a zipfile entry with Zstd compression, but " ++- "the zip library has been built with zstd disabled."-#endif--instance Exception ZipException
README.md view
@@ -4,9 +4,9 @@ [](https://hackage.haskell.org/package/zip) [](http://stackage.org/nightly/package/zip) [](http://stackage.org/lts/package/zip)-+[](https://github.com/mrkkrp/zip/actions/workflows/ci.yaml) -* [Why this library is written](#why-this-library-is-written)+* [Why this library was written](#why-this-library-was-written) * [zip-archive](#zip-archive) * [LibZip](#libzip) * [zip-conduit](#zip-conduit)@@ -22,13 +22,11 @@ * [License](#license) This is a feature-rich, memory-efficient, and type-safe library to-manipulate Zip archives. The library is the most complete and efficient-implementation of the .ZIP specification in Haskell (at least from the-open-sourced ones). In particular, it's created with large multimedia data+manipulate Zip archives. The library was created with large multimedia data in mind and provides all features users might expect, comparable in terms of feature-set with libraries like `libzip` in C. -## Why this library is written+## Why this library was written There are a few libraries to work with Zip archives, yet every one of them provides only a subset of useful functionality or otherwise is flawed in
− Setup.hs
@@ -1,6 +0,0 @@-module Main (main) where--import Distribution.Simple--main :: IO ()-main = defaultMain
tests/Main.hs view
@@ -13,32 +13,41 @@ import Control.Monad.IO.Class import Data.Bits import Data.ByteString (ByteString)-import qualified Data.ByteString as B-import qualified Data.ByteString.Builder as LB-import qualified Data.ByteString.Lazy as LB-import qualified Data.Conduit as C-import qualified Data.Conduit.List as CL-import qualified Data.DList as DList+import Data.ByteString qualified as B+import Data.ByteString.Builder qualified as LB+import Data.ByteString.Lazy qualified as LB+import Data.Conduit qualified as C+import Data.Conduit.List qualified as CL+import Data.DList qualified as DList import Data.List (intercalate) import Data.Map (Map, (!))-import qualified Data.Map.Strict as M+import Data.Map.Strict qualified as M import Data.Maybe (fromJust)-import qualified Data.Set as E+import Data.Set qualified as E import Data.Text (Text)-import qualified Data.Text as T-import qualified Data.Text.Encoding as T+import Data.Text qualified as T+import Data.Text.Encoding qualified as T import Data.Time import Data.Version import Data.Word+import Numeric.Natural import System.Directory import System.FilePath ((</>))-import qualified System.FilePath as FP+import System.FilePath qualified as FP import System.IO import System.IO.Error (isDoesNotExistError) import System.IO.Temp import Test.Hspec import Test.QuickCheck hiding ((.&.)) +ffffffff :: Natural++#ifdef HASKELL_ZIP_DEV_MODE+ffffffff = 250+#else+ffffffff = 0xffffffff+#endif+ -- | Zip tests. Please note that the Zip64 feature is not currently tested -- automatically because we'd need > 4GB of data. Handling such quantities -- of data locally is problematic and even more problematic on CI.@@ -82,7 +91,7 @@ arbitrary = T.pack <$> listOf1 arbitrary instance Arbitrary ByteString where- arbitrary = B.pack <$> listOf arbitrary+ arbitrary = B.pack <$> scale (* 10) (listOf arbitrary) {- ORMOLU_DISABLE -} @@ -118,7 +127,8 @@ resize 10 $ intercalate "/" <$> listOf1- ( (++) <$> vectorOf 3 charGen+ ( (++)+ <$> vectorOf 3 charGen <*> listOf1 charGen ) case mkEntrySelector p of@@ -194,21 +204,6 @@ (1, return mempty) ] -instance Show EntryDescription where- show ed =- "{ edCompression = " ++ show (edCompression ed)- ++ "\n, edModTime = "- ++ show (edModTime ed)- ++ "\n, edUncompressedSize = "- ++ show (edUncompressedSize ed)- ++ "\n, edComment = "- ++ show (edComment ed)- ++ "\n, edExtraField = "- ++ show (edExtraField ed)- ++ "\n, edExtFileAttr = "- ++ show (edExternalFileAttrs ed)- ++ " }"- instance Show (ZipArchive a) where show = const "<zip archive>" @@ -383,25 +378,37 @@ -- it should be mentioned that the version also depends on Zip64 feature property $ \(EM s desc z) -> do desc' <- fromJust <$> createArchive path (z >> commit >> getEntryDesc s)- edVersionNeeded desc'- `shouldBe` makeVersion- ( case edCompression desc of- Store -> [2, 0]- Deflate -> [2, 0]- BZip2 -> [4, 6]- Zstd -> [6, 3]- )+ let minVersionZip64 =+ makeVersion $+ if edUncompressedSize desc' >= ffffffff+ || edCompressedSize desc' >= ffffffff+ then [4, 5]+ else [2, 0]+ minVersionCompression = makeVersion $ case edCompression desc of+ Store -> [2, 0]+ Deflate -> [2, 0]+ BZip2 -> [4, 6]+ Zstd -> [6, 3]+ versionNeeded = max minVersionZip64 minVersionCompression+ edVersionNeeded desc' `shouldBe` versionNeeded addEntrySpec :: SpecWith FilePath addEntrySpec =- context "when an entry is added" $+ context "when an entry is added" $ do it "is there" $ \path -> property $ \m b s -> do info <- createArchive path $ do addEntry m b s commit+ checkEntry' s (,) <$> getEntry s <*> (edCompression . (! s) <$> getEntries) info `shouldBe` (b, m)+ it "does not add unnecessary zip64 extra fields" $ \path -> do+ property $ \b s ->+ fromIntegral (B.length b) < ffffffff ==> do+ createArchive path (addEntry Store b s)+ bytes <- B.drop (30 + rawSelectorLength s) <$> B.readFile path+ bytes `shouldSatisfy` B.isPrefixOf b sinkEntrySpec :: SpecWith FilePath sinkEntrySpec =@@ -411,7 +418,9 @@ info <- createArchive path $ do sinkEntry m (C.yield b) s commit- (,) <$> sourceEntry s (CL.foldMap id)+ checkEntry' s+ (,)+ <$> sourceEntry s (CL.foldMap id) <*> (edCompression . (! s) <$> getEntries) info `shouldBe` (b, m) @@ -426,6 +435,7 @@ createArchive path $ do loadEntry m s vpath commit+ checkEntry' s liftIO (removeFile vpath) saveEntry s vpath B.readFile vpath `shouldReturn` b@@ -434,7 +444,7 @@ copyEntrySpec :: SpecWith FilePath copyEntrySpec =- context "when entry is copied form another archive" $+ context "when entry is copied from another archive" $ do it "is there" $ \path -> property $ \m b s -> do let vpath = deriveVacant path@@ -442,37 +452,76 @@ info <- createArchive path $ do copyEntry vpath s s commit+ checkEntry' s (,) <$> getEntry s <*> (edCompression . (! s) <$> getEntries) info `shouldBe` (b, m)+ it "does not add unnecessary zip64 extra fields" $ \path -> do+ property $ \b s ->+ fromIntegral (B.length b) < ffffffff ==> do+ let vpath = deriveVacant path+ createArchive vpath $ addEntry Store b s+ createArchive path $ copyEntry vpath s s+ bytes <- B.drop (30 + rawSelectorLength s) <$> B.readFile path+ bytes `shouldSatisfy` B.isPrefixOf b checkEntrySpec :: SpecWith FilePath checkEntrySpec = do- context "when entry is intact" $- it "passes the check" $ \path ->- property $ \m b s -> do- check <- createArchive path $ do- addEntry m b s- commit- checkEntry s- check `shouldBe` True- context "when entry is corrupted" $- it "does not pass the check" $ \path ->- property $ \b s ->- not (B.null b) ==> do- let headerLength = 30 + (B.length . T.encodeUtf8 . getEntryName $ s)- localFileHeaderOffset <- createArchive path $ do- addEntry Store b s+ context "for entries added via addEntry" $ do+ context "when entry is intact" $+ it "passes the check" $ \path ->+ property $ \m b s ->+ asIO . createArchive path $ do+ addEntry m b s commit- fromIntegral . edOffset . (! s) <$> getEntries- withFile path ReadWriteMode $ \h -> do- hSeek- h- AbsoluteSeek- (localFileHeaderOffset + fromIntegral headerLength)- byte <- B.map complement <$> B.hGet h 1- hSeek h RelativeSeek (-1)- B.hPut h byte- withArchive path (checkEntry s) `shouldReturn` False+ checkEntry' s+ context "when entry is corrupted" $+ it "does not pass the check" $ \path ->+ property $ \b s ->+ not (B.null b) ==> do+ let zip64Length =+ if fromIntegral (B.length b) >= ffffffff+ then 20+ else 0+ headerLength = 30 + rawSelectorLength s + zip64Length+ localFileHeaderOffset <- createArchive path $ do+ addEntry Store b s+ commit+ fromIntegral . edOffset . (! s) <$> getEntries+ withFile path ReadWriteMode $ \h -> do+ hSeek+ h+ AbsoluteSeek+ (localFileHeaderOffset + fromIntegral headerLength)+ byte <- B.map complement <$> B.hGet h 1+ hSeek h RelativeSeek (-1)+ B.hPut h byte+ withArchive path (checkEntry s) `shouldReturn` False+ context "for entries added via sinkEntry" $ do+ context "when entry is intact" $+ it "passes the check" $ \path ->+ property $ \m b s ->+ asIO . createArchive path $ do+ sinkEntry m (C.yield b) s+ commit+ checkEntry' s+ context "when entry is corrupted" $+ it "does not pass the check" $ \path ->+ property $ \b s ->+ not (B.null b) ==> do+ let headerLength = 50 + rawSelectorLength s+ localFileHeaderOffset <- createArchive path $ do+ sinkEntry Store (C.yield b) s+ commit+ fromIntegral . edOffset . (! s) <$> getEntries+ withFile path ReadWriteMode $ \h -> do+ hSeek+ h+ AbsoluteSeek+ (localFileHeaderOffset + fromIntegral headerLength)+ byte <- B.map complement <$> B.hGet h 1+ hSeek h RelativeSeek (-1)+ B.hPut h byte+ withArchive path (checkEntry s) `shouldReturn` False recompressSpec :: SpecWith FilePath recompressSpec =@@ -482,8 +531,10 @@ info <- createArchive path $ do addEntry m b s commit+ checkEntry' s recompress m' s commit+ checkEntry' s (,) <$> getEntry s <*> (edCompression . (! s) <$> getEntries) info `shouldBe` (b, m') @@ -793,6 +844,12 @@ withSandbox action = withSystemTempDirectory "zip-sandbox" $ \dir -> action (dir </> "foo.zip") +-- | Like 'checkEntry' but automatically aborts the test if the check fails.+checkEntry' :: EntrySelector -> ZipArchive ()+checkEntry' s = do+ r <- checkEntry s+ liftIO (if r then return () else fail "Entry integrity check failed!")+ -- | Given a primary name (name of archive), generate a name that does not -- collide with it. deriveVacant :: FilePath -> FilePath@@ -872,3 +929,11 @@ if isDir then go adir' else return mempty++-- | Constrain the type of the argument monad to 'IO'.+asIO :: IO a -> IO a+asIO = id++-- | Get the length in bytes of an encoded 'EntrySelector'.+rawSelectorLength :: EntrySelector -> Int+rawSelectorLength = B.length . T.encodeUtf8 . getEntryName
zip.cabal view
@@ -1,11 +1,11 @@-cabal-version: 1.18+cabal-version: 2.4 name: zip-version: 1.7.2-license: BSD3+version: 2.2.2+license: BSD-3-Clause license-file: LICENSE.md maintainer: Mark Karpov <markkarpov92@gmail.com> author: Mark Karpov <markkarpov92@gmail.com>-tested-with: ghc ==8.8.4 ghc ==8.10.5 ghc ==9.0.1+tested-with: ghc ==9.10.3 ghc ==9.12.4 ghc ==9.14.1 homepage: https://github.com/mrkkrp/zip bug-reports: https://github.com/mrkkrp/zip/issues synopsis: Operations on zip archives@@ -44,31 +44,29 @@ Codec.Archive.Zip Codec.Archive.Zip.CP437 Codec.Archive.Zip.Unix-- other-modules: Codec.Archive.Zip.Internal- Codec.Archive.Zip.Type+ Codec.Archive.Zip.Internal.Type - default-language: Haskell2010+ default-language: GHC2021 build-depends:- base >=4.13 && <5.0,- bytestring >=0.9 && <0.12,+ base >=4.15 && <5,+ bytestring >=0.9 && <0.13, case-insensitive >=1.2.0.2 && <1.3, cereal >=0.3 && <0.6, conduit >=1.3 && <1.4, conduit-extra >=1.3 && <1.4,- containers >=0.5 && <0.7,+ containers >=0.5 && <0.9, digest <0.1, directory >=1.2.2 && <1.4, dlist >=0.8 && <2.0, exceptions >=0.6 && <0.11,- filepath >=1.2 && <1.5,+ filepath >=1.2 && <1.6, monad-control >=1.0 && <1.1,- mtl >=2.0 && <3.0,- resourcet >=1.2 && <1.3,- text >=0.2 && <1.3,- time >=1.4 && <1.13,- transformers >=0.4 && <0.6,+ mtl >=2 && <3,+ resourcet >=1.2 && <1.4,+ text >=0.2 && <2.2,+ time >=1.4 && <1.16,+ transformers >=0.4 && <0.7, transformers-base if !flag(disable-bzip2)@@ -80,8 +78,8 @@ if flag(dev) cpp-options: -DHASKELL_ZIP_DEV_MODE ghc-options:- -O0 -Wall -Werror -Wcompat -Wincomplete-record-updates- -Wincomplete-uni-patterns -Wnoncanonical-monad-instances+ -Wall -Werror -Wredundant-constraints -Wpartial-fields+ -Wunused-packages else ghc-options: -O2 -Wall@@ -97,21 +95,21 @@ else cpp-options: -DZIP_OS=3- build-depends: unix <2.8+ build-depends: unix <2.9 executable haskell-zip-app main-is: Main.hs hs-source-dirs: bench-app- default-language: Haskell2010+ default-language: GHC2021 build-depends:- base >=4.13 && <5.0,- filepath >=1.2 && <1.5,+ base >=4.15 && <5,+ filepath >=1.2 && <1.6, zip if flag(dev) ghc-options:- -Wall -Werror -Wcompat -Wincomplete-record-updates- -Wincomplete-uni-patterns -Wnoncanonical-monad-instances+ -Wall -Werror -Wredundant-constraints -Wpartial-fields+ -Wunused-packages else ghc-options: -O2 -Wall@@ -120,26 +118,27 @@ type: exitcode-stdio-1.0 main-is: Main.hs hs-source-dirs: tests- default-language: Haskell2010+ default-language: GHC2021 build-depends:- base >=4.13 && <5.0,- QuickCheck >=2.4 && <3.0,- bytestring >=0.9 && <0.12,+ base >=4.15 && <5,+ QuickCheck >=2.4 && <3,+ bytestring >=0.9 && <0.13, conduit >=1.3 && <1.4,- containers >=0.5 && <0.7,+ containers >=0.5 && <0.9, directory >=1.2.2 && <1.4,- dlist >=0.8 && <2.0,- exceptions >=0.6 && <0.11,- filepath >=1.2 && <1.5,- hspec >=2.0 && <3.0,+ dlist >=0.8 && <2,+ filepath >=1.2 && <1.6,+ hspec >=2 && <3, temporary >=1.1 && <1.4,- text >=0.2 && <1.3,- time >=1.4 && <1.13,- transformers >=0.4 && <0.6,+ text >=0.2 && <2.2,+ time >=1.4 && <1.16, zip if flag(dev)- ghc-options: -O0 -Wall -Werror+ cpp-options: -DHASKELL_ZIP_DEV_MODE+ ghc-options:+ -Wall -Werror -Wredundant-constraints -Wpartial-fields+ -Wunused-packages else ghc-options: -O2 -Wall