packages feed

zip 1.7.2 → 2.2.2

raw patch · 10 files changed

Files

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 @@ [![Hackage](https://img.shields.io/hackage/v/zip.svg?style=flat)](https://hackage.haskell.org/package/zip) [![Stackage Nightly](http://stackage.org/package/zip/badge/nightly)](http://stackage.org/nightly/package/zip) [![Stackage LTS](http://stackage.org/package/zip/badge/lts)](http://stackage.org/lts/package/zip)-![CI](https://github.com/mrkkrp/zip/workflows/CI/badge.svg?branch=master)+[![CI](https://github.com/mrkkrp/zip/actions/workflows/ci.yaml/badge.svg)](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