diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/Codec/Archive/Zip.hs b/Codec/Archive/Zip.hs
--- a/Codec/Archive/Zip.hs
+++ b/Codec/Archive/Zip.hs
@@ -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 ::
diff --git a/Codec/Archive/Zip/CP437.hs b/Codec/Archive/Zip/CP437.hs
--- a/Codec/Archive/Zip/CP437.hs
+++ b/Codec/Archive/Zip/CP437.hs
@@ -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.
diff --git a/Codec/Archive/Zip/Internal.hs b/Codec/Archive/Zip/Internal.hs
--- a/Codec/Archive/Zip/Internal.hs
+++ b/Codec/Archive/Zip/Internal.hs
@@ -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
diff --git a/Codec/Archive/Zip/Internal/Type.hs b/Codec/Archive/Zip/Internal/Type.hs
new file mode 100644
--- /dev/null
+++ b/Codec/Archive/Zip/Internal/Type.hs
@@ -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
diff --git a/Codec/Archive/Zip/Type.hs b/Codec/Archive/Zip/Type.hs
deleted file mode 100644
--- a/Codec/Archive/Zip/Type.hs
+++ /dev/null
@@ -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
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/Setup.hs b/Setup.hs
deleted file mode 100644
--- a/Setup.hs
+++ /dev/null
@@ -1,6 +0,0 @@
-module Main (main) where
-
-import Distribution.Simple
-
-main :: IO ()
-main = defaultMain
diff --git a/tests/Main.hs b/tests/Main.hs
--- a/tests/Main.hs
+++ b/tests/Main.hs
@@ -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
diff --git a/zip.cabal b/zip.cabal
--- a/zip.cabal
+++ b/zip.cabal
@@ -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
