packages feed

zip 0.1.11 → 2.2.2

raw patch · 13 files changed

Files

CHANGELOG.md view
@@ -1,3 +1,132 @@+## 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+  it was written unconditionally and it confused some tools.++## Zip 1.7.1++* Fixed compilation with zstd and/or bzip2 disabled.++## Zip 1.7.0++* Set user permissions on linux platform as follows: if an existing file is+  added, use its permissions; if an entry is generated from a bytestring or+  a stream, use 0600. This behavior mimics the zip utility.++## Zip 1.6.0++* Added support for Zstandard (zstd) compression++* Added a Cabal flag `-fdisable-zstd` to remove the zstd C library+  dependency and hence support for Zstd entries.++## Zip 1.5.0++* Added the `packDirRecur'` function.++* Dropped support for GHC 8.4.++## Zip 1.4.1++* Fixed the build on Mac.++## Zip 1.4.0++* The “version made by” info inside archive now correctly sets Unix as the+  OS that produced the archive when the library is compiled on Unix. This+  allows other utilities such as `unzip` to read and correctly restore file+  permissions. [Issue 62](https://github.com/mrkkrp/zip/issues/62).++* Added the `Codec.Archive.Zip.Unix` module.++## Zip 1.3.2++* Fix a bug where removing a temporary file failed in the prescence of+  async exceptions.++## Zip 1.3.1++* The test suite is now faster.++## Zip 1.3.0++* Dropped support for GHC 8.2 and older.++* Added a Cabal flag `-fdisable-bzip2` to remove the bzip2 C library+  dependency and hence support for BZip2 entries.++## Zip 1.2.0++* Added the `setExternalFileAttrs` function and the `edExternalFileAttrs`+  field in the `EntryDescription` record.++## Zip 1.1.0++* Made `saveEntry` and `unpackInto` restore modification time of files.++## Zip 1.0.0++* Works with `conduit-1.3.0`, `conduit-extra-1.3.0`, `resourcet-1.2.0` and+  `bzlib-conduit-0.3.0`.++* Stop depending on `path`, `path-io`, and `plub-b`.++* Made the module `Codec.Archive.Zip.Type` non-public.++* Remove derived instances of `Data` and `Generic` for `EntrySelector` not+  to expose its inner structure.++* Change signature of the `loadEntry` function, its second argument is now+  just `EntrySelector` of the entry to add.++* The second argument of `packDirRecur` now receives paths that are relative+  to the root of the directory we pack.++## Zip 0.2.0++* Added `MonadBase` and `MonadBaseControl` instances for the `ZipArchive`+  monad. Also exported the `ZipState` type without revealing its data+  constructor and records.++* Dropped `MonadThrow` and `MonadCatch` constraints for `createArchive` and+  `withArchive`.+ ## Zip 0.1.11  * Minor refactoring.
Codec/Archive/Zip.hs view
@@ -1,6 +1,13 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE TypeFamilies #-}+ -- | -- Module      :  Codec.Archive.Zip--- Copyright   :  © 2016–2017 Mark Karpov+-- Copyright   :  © 2016–present Mark Karpov -- License     :  BSD 3 clause -- -- Maintainer  :  Mark Karpov <markkarpov92@gmail.com>@@ -9,157 +16,170 @@ -- -- The module provides everything you may need to manipulate Zip archives. -- There are three things that should be clarified right away, to avoid--- confusion in the future.+-- confusion. ----- First, we use the 'EntrySelector' type that can be obtained from 'Path'--- 'Rel' 'File' paths. This method may seem awkward at first, but it will--- protect you from the problems with portability when your archive is--- unpacked on a different platform. Using well-typed paths is also--- something you should consider doing in your projects anyway. Even if you--- don't want to use the "Path" module in your project, it's easy to marshal--- 'FilePath' to 'Path' just before using functions from the library.+-- First, we use the 'EntrySelector' type that can be obtained from relative+-- 'FilePath's (paths to directories are not allowed). This method may seem+-- awkward at first, but it will protect you from the problems with+-- portability when your archive is unpacked on a different platform. ----- The second thing, that is rather a consequence of the first, is that--- there is no way to add directories, or to be precise, /empty directories/--- to your archive. This approach is used in Git, and I find it quite sane.+-- Second, there is no way to add directories, or to be precise, /empty+-- directories/ to your archive. This approach is used in Git, and I find it+-- sane. -- -- Finally, the third feature of the library is that it does not modify -- archive instantly, because doing so on every manipulation would often be--- inefficient. Instead we maintain a collection of pending actions that can--- be turned into an optimized procedure that efficiently modifies archive--- in one pass. Normally this should be of no concern to you, because all--- actions are performed automatically when you leave the realm of+-- inefficient. Instead, we maintain a collection of pending actions that+-- can be turned into an optimized procedure that efficiently modifies the+-- archive in one pass. Normally, this should be of no concern to you,+-- because all actions are performed automatically when you leave the -- 'ZipArchive' monad. If, however, you ever need to force an update, the--- 'commit' function is your friend. There are even “undo” functions, by the--- way.+-- 'commit' function is your friend. -- -- === Examples -- -- An example of a program that prints a list of archive entries: -- -- > import Codec.Archive.Zip--- > import Path.IO (resolveFile') -- > import System.Environment (getArgs) -- > import qualified Data.Map as M -- > -- > main :: IO () -- > main = do--- >   [fp]    <- getArgs--- >   path    <- resolveFile' fp+-- >   [path]  <- getArgs -- >   entries <- withArchive path (M.keys <$> getEntries) -- >   mapM_ print entries -- -- Create a Zip archive with a “Hello World” file: -- -- > import Codec.Archive.Zip--- > import Path (parseRelFile)--- > import Path.IO (resolveFile') -- > import System.Environment (getArgs) -- > -- > main :: IO () -- > main = do--- >   [fp]    <- getArgs--- >   path    <- resolveFile' fp--- >   s       <- parseRelFile "hello-world.txt" >>= mkEntrySelector+-- >   [path] <- getArgs+-- >   s      <- mkEntrySelector "hello-world.txt" -- >   createArchive path (addEntry Store "Hello, World!" s) ----- Extract contents of a specific file and print them:+-- Extract contents of a file and print them: -- -- > import Codec.Archive.Zip--- > import Path (parseRelFile)--- > import Path.IO (resolveFile') -- > import System.Environment (getArgs) -- > import qualified Data.ByteString.Char8 as B -- > -- > main :: IO () -- > main = do--- >   [fp,f]  <- getArgs--- >   path    <- resolveFile' fp--- >   s       <- parseRelFile f >>= mkEntrySelector--- >   bs      <- withArchive path (getEntry s)+-- >   [path,f] <- getArgs+-- >   s        <- mkEntrySelector f+-- >   bs       <- withArchive path (getEntry s) -- >   B.putStrLn bs--{-# LANGUAGE GeneralizedNewtypeDeriving #-}- module Codec.Archive.Zip   ( -- * Types+     -- ** Entry selector-    EntrySelector-  , mkEntrySelector-  , unEntrySelector-  , getEntryName-  , EntrySelectorException (..)+    EntrySelector,+    mkEntrySelector,+    unEntrySelector,+    getEntryName,+    EntrySelectorException (..),+     -- ** Entry description-  , EntryDescription (..)-  , CompressionMethod (..)+    EntryDescription (..),+    CompressionMethod (..),+     -- ** Archive description-  , ArchiveDescription (..)+    ArchiveDescription (..),+     -- ** Exceptions-  , ZipException (..)+    ZipException (..),+     -- * Archive monad-  , ZipArchive-  , createArchive-  , withArchive+    ZipArchive,+    ZipState,+    createArchive,+    withArchive,+     -- * Retrieving information-  , getEntries-  , doesEntryExist-  , getEntryDesc-  , getEntry-  , getEntrySource-  , sourceEntry-  , saveEntry-  , checkEntry-  , unpackInto-  , getArchiveComment-  , getArchiveDescription+    getEntries,+    doesEntryExist,+    getEntryDesc,+    getEntry,+    getEntrySource,+    sourceEntry,+    saveEntry,+    checkEntry,+    unpackInto,+    getArchiveComment,+    getArchiveDescription,+     -- * Modifying archive+     -- ** Adding entries-  , addEntry-  , sinkEntry-  , loadEntry-  , copyEntry-  , packDirRecur+    addEntry,+    sinkEntry,+    loadEntry,+    copyEntry,+    packDirRecur,+    packDirRecur',+     -- ** Modifying entries-  , renameEntry-  , deleteEntry-  , recompress-  , setEntryComment-  , deleteEntryComment-  , setModTime-  , addExtraField-  , deleteExtraField-  , forEntries+    renameEntry,+    deleteEntry,+    recompress,+    setEntryComment,+    deleteEntryComment,+    setModTime,+    addExtraField,+    deleteExtraField,+    setExternalFileAttrs,+    forEntries,+     -- ** Operations on archive as a whole-  , setArchiveComment-  , deleteArchiveComment+    setArchiveComment,+    deleteArchiveComment,+     -- ** Control over editing-  , undoEntryChanges-  , undoArchiveChanges-  , undoAll-  , commit )+    undoEntryChanges,+    undoArchiveChanges,+    undoAll,+    commit,+  ) where -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 (..)) import Control.Monad.Catch import Control.Monad.State.Strict-import Control.Monad.Trans.Resource (ResourceT, runResourceT, MonadResource)+import Control.Monad.Trans.Control (MonadBaseControl (..))+import Control.Monad.Trans.Resource (MonadResource, ResourceT) import Data.ByteString (ByteString)-import Data.Conduit (Source, Sink, ($$), yield)+import Data.Conduit (ConduitT, (.|))+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 Data.Map.Strict qualified as M import Data.Sequence (Seq, (|>))+import Data.Sequence qualified as S+import Data.Set qualified as E import Data.Text (Text) import Data.Time.Clock (UTCTime)-import Data.Word (Word16)-import Path-import Path.IO-import qualified Codec.Archive.Zip.Internal as I-import qualified Data.Conduit.Binary        as CB-import qualified Data.Conduit.List          as CL-import qualified Data.Map.Strict            as M-import qualified Data.Sequence              as S-import qualified Data.Set                   as E+import Data.Void+import Data.Word (Word16, Word32)+import System.Directory+import System.FilePath ((</>))+import System.FilePath qualified as FP+import System.IO.Error (isDoesNotExistError) +#ifndef mingw32_HOST_OS+import qualified Codec.Archive.Zip.Unix as Unix+import qualified System.Posix as Unix+#endif+ ---------------------------------------------------------------------------- -- Archive monad @@ -167,49 +187,70 @@ -- archives. It's intentionally opaque and not a monad transformer to limit -- the actions that can be performed in it to those provided by this module -- and their combinations.- newtype ZipArchive a = ZipArchive   { unZipArchive :: StateT ZipState IO a-  } deriving ( Functor-             , Applicative-             , Monad-             , MonadIO-             , MonadThrow-             , MonadCatch-             , MonadMask )+  }+  deriving+    ( Functor,+      Applicative,+      Monad,+      MonadIO,+      MonadThrow,+      MonadCatch,+      MonadMask+    ) --- | Internal state record used by the 'ZipArchive' monad.+-- | @since 0.2.0+instance MonadBase IO ZipArchive where+  liftBase = liftIO +-- | @since 0.2.0+instance MonadBaseControl IO ZipArchive where+  type StM ZipArchive a = (a, ZipState)+  liftBaseWith f = ZipArchive . StateT $ \s ->+    (,s) <$> f (flip runStateT s . unZipArchive)+  {-# INLINEABLE liftBaseWith #-}+  restoreM = ZipArchive . StateT . const . return+  {-# INLINEABLE restoreM #-}++-- | The internal state record used by the 'ZipArchive' monad. This is only+-- exported for use with 'MonadBaseControl' methods, you can't look inside.+--+-- @since 0.2.0 data ZipState = ZipState-  { zsFilePath  :: Path Abs File-    -- ^ Absolute path to zip archive-  , zsEntries   :: Map EntrySelector EntryDescription-    -- ^ Actual collection of entries-  , zsArchive   :: ArchiveDescription-    -- ^ Info about the whole archive-  , zsActions   :: Seq I.PendingAction-    -- ^ Pending actions+  { -- | Path to zip archive+    zsFilePath :: FilePath,+    -- | Actual collection of entries+    zsEntries :: Map EntrySelector EntryDescription,+    -- | Info about the whole archive+    zsArchive :: ArchiveDescription,+    -- | Pending actions+    zsActions :: Seq I.PendingAction   }  -- | Create a new archive given its location and an action that describes -- how to create contents of the archive. This will silently overwrite the -- specified file if it already exists. See 'withArchive' if you want to -- work with an existing archive.--createArchive :: (MonadIO m, MonadCatch m)-  => Path b File       -- ^ Location of archive file to create-  -> ZipArchive a      -- ^ Actions that form archive's content-  -> m a-createArchive path m = do+createArchive ::+  (MonadIO m) =>+  -- | Location of the archive file to create+  FilePath ->+  -- | Actions that create the archive's content+  ZipArchive a ->+  m a+createArchive path m = liftIO $ do   apath <- makeAbsolute path   ignoringAbsence (removeFile apath)-  let st = ZipState-        { zsFilePath = apath-        , zsEntries  = M.empty-        , zsArchive  = ArchiveDescription Nothing 0 0-        , zsActions  = S.empty }-      action = unZipArchive (liftM2 const m commit)-  liftIO (evalStateT action st)+  let st =+        ZipState+          { zsFilePath = apath,+            zsEntries = M.empty,+            zsArchive = ArchiveDescription Nothing 0 0,+            zsActions = S.empty+          }+      action = unZipArchive (m <* commit)+  evalStateT action st  -- | Work with an existing archive. See 'createArchive' if you want to -- create a new archive instead.@@ -235,34 +276,37 @@ -- 'EntrySelector' is case-insensitive. These are the consequences of the -- design decision to make it impossible to create non-portable archives -- with this library.--withArchive :: (MonadIO m, MonadThrow m)-  => Path b File       -- ^ Location of archive to work with-  -> ZipArchive a      -- ^ Actions on that archive-  -> m a-withArchive path m = do-  apath           <- canonicalizePath path+withArchive ::+  (MonadIO m) =>+  -- | Location of the archive to work with+  FilePath ->+  -- | Actions on that archive+  ZipArchive a ->+  m a+withArchive path m = liftIO $ do+  apath <- canonicalizePath path   (desc, entries) <- liftIO (I.scanArchive apath)-  let st = ZipState-        { zsFilePath = apath-        , zsEntries  = entries-        , zsArchive  = desc-        , zsActions  = S.empty }-      action = unZipArchive (liftM2 const m commit)+  let st =+        ZipState+          { zsFilePath = apath,+            zsEntries = entries,+            zsArchive = desc,+            zsActions = S.empty+          }+      action = unZipArchive (m <* commit)   liftIO (evalStateT action st)  ---------------------------------------------------------------------------- -- Retrieving information --- | Retrieve description of all archive entries. This is an efficient--- operation that can be used for example to list all entries in an archive.--- Do not hesitate to use the function frequently: scanning of archive--- happens only once anyway.+-- | Retrieve a description of all archive entries. This is an efficient+-- operation that can be used for example to list all entries in the+-- archive. Do not hesitate to use the function frequently: scanning of the+-- archive happens only once. ----- Please note that the returned value only reflects actual contents of the--- archive in file system, non-committed actions do not influence the list--- of entries, see 'commit' for more information.-+-- Please note that the returned value only reflects the current contents of+-- the archive in file system, non-committed actions are not reflected, see+-- 'commit' for more information. getEntries :: ZipArchive (Map EntrySelector EntryDescription) getEntries = ZipArchive (gets zsEntries) @@ -270,15 +314,13 @@ -- simple shortcut defined as: -- -- > doesEntryExist s = M.member s <$> getEntries- doesEntryExist :: EntrySelector -> ZipArchive Bool doesEntryExist s = M.member s <$> getEntries --- | Get 'EntryDescription' for specified entry. This is a simple shortcut+-- | Get 'EntryDescription' for a specified entry. This is a simple shortcut -- defined as: -- -- > getEntryDesc s = M.lookup s <$> getEntries- getEntryDesc :: EntrySelector -> ZipArchive (Maybe EntryDescription) getEntryDesc s = M.lookup s <$> getEntries @@ -287,10 +329,11 @@ -- lot of memory. For big entries, use conduits: 'sourceEntry'. -- -- Throws: 'EntryDoesNotExist'.--getEntry-  :: EntrySelector     -- ^ Selector that identifies archive entry-  -> ZipArchive ByteString -- ^ Contents of the entry+getEntry ::+  -- | Selector that identifies archive entry+  EntrySelector ->+  -- | Contents of the entry+  ZipArchive ByteString getEntry s = sourceEntry s (CL.foldMap id)  -- | Get an entry source.@@ -298,80 +341,81 @@ -- Throws: 'EntryDoesNotExist'. -- -- @since 0.1.3--getEntrySource-  :: MonadResource m-  => EntrySelector-  -> ZipArchive (Source m ByteString)+getEntrySource ::+  (PrimMonad m, MonadThrow m, MonadResource m) =>+  -- | Selector that identifies archive entry+  EntrySelector ->+  ZipArchive (ConduitT () ByteString m ()) getEntrySource s = do-  path  <- getFilePath+  path <- getFilePath   mdesc <- M.lookup s <$> getEntries   case mdesc of-    Nothing   -> throwM (EntryDoesNotExist path s)+    Nothing -> throwM (EntryDoesNotExist path s)     Just desc -> return (I.sourceEntry path desc True)  -- | Stream contents of an archive entry to the given 'Sink'. -- -- Throws: 'EntryDoesNotExist'.--sourceEntry-  :: EntrySelector-     -- ^ Selector that identifies archive entry-  -> Sink ByteString (ResourceT IO) a-     -- ^ Sink where to stream entry contents-  -> ZipArchive a-     -- ^ Contents of the entry (if found)+sourceEntry ::+  -- | Selector that identifies the archive entry+  EntrySelector ->+  -- | Sink where to stream entry contents+  ConduitT ByteString Void (ResourceT IO) a ->+  -- | Contents of the entry (if found)+  ZipArchive a sourceEntry s sink = do   src <- getEntrySource s-  (liftIO . runResourceT) (src $$ sink)+  (liftIO . C.runConduitRes) (src .| sink)  -- | Save a specific archive entry as a file in the file system. -- -- Throws: 'EntryDoesNotExist'.--saveEntry-  :: EntrySelector     -- ^ Selector that identifies archive entry-  -> Path b File       -- ^ Where to save the file-  -> ZipArchive ()-saveEntry s path = sourceEntry s (CB.sinkFile (toFilePath path))+saveEntry ::+  -- | Selector that identifies the archive entry+  EntrySelector ->+  -- | Where to save the file+  FilePath ->+  ZipArchive ()+saveEntry s path = do+  sourceEntry s (CB.sinkFile path)+  med <- getEntryDesc s+  forM_ med (liftIO . setModificationTime path . edModTime)  -- | Calculate CRC32 check sum and compare it with the value read from the -- archive. The function returns 'True' when the check sums are the -- same—that is, the data is not corrupted. -- -- Throws: 'EntryDoesNotExist'.--checkEntry-  :: EntrySelector     -- ^ Selector that identifies archive entry-  -> ZipArchive Bool   -- ^ Is the entry intact?+checkEntry ::+  -- | Selector that identifies the archive entry+  EntrySelector ->+  -- | Is the entry intact?+  ZipArchive Bool checkEntry s = do   calculated <- sourceEntry s I.crc32Sink-  given      <- edCRC32 . (! s) <$> getEntries-  -- ↑ NOTE We can assume that entry exists for sure because otherwise+  given <- edCRC32 . (! s) <$> getEntries+  -- NOTE We can assume that entry exists for sure because otherwise   -- 'sourceEntry' would have thrown 'EntryDoesNotExist' already.   return (calculated == given) --- | Unpack the entire archive into the specified directory. The directory--- will be created if it does not exist.--unpackInto :: Path b Dir -> ZipArchive ()+-- | Unpack the archive into the specified directory. The directory will be+-- created if it does not exist.+unpackInto :: FilePath -> ZipArchive () unpackInto dir' = do   selectors <- M.keysSet <$> getEntries   unless (null selectors) $ do     dir <- liftIO (makeAbsolute dir')-    liftIO (ensureDir dir)-    let dirs = E.map (parent . (dir </>) . unEntrySelector) selectors-    forM_ dirs (liftIO . ensureDir)+    liftIO (createDirectoryIfMissing True dir)+    let dirs = E.map (FP.takeDirectory . (dir </>) . unEntrySelector) selectors+    forM_ dirs (liftIO . createDirectoryIfMissing True)     forM_ selectors $ \s ->       saveEntry s (dir </> unEntrySelector s)  -- | Get the archive comment.- getArchiveComment :: ZipArchive (Maybe Text) getArchiveComment = adComment <$> getArchiveDescription  -- | Get the archive description record.- getArchiveDescription :: ZipArchive ArchiveDescription getArchiveDescription = ZipArchive (gets zsArchive) @@ -379,215 +423,294 @@ -- Modifying archive  -- | Add a new entry to the archive given its contents in binary form.--addEntry-  :: CompressionMethod -- ^ Compression method to use-  -> ByteString        -- ^ Entry contents-  -> EntrySelector     -- ^ Name of entry to add-  -> ZipArchive ()-addEntry t b s = addPending (I.SinkEntry t (yield b) s)+addEntry ::+  -- | The compression method to use+  CompressionMethod ->+  -- | Entry contents+  ByteString ->+  -- | Name of the entry to add+  EntrySelector ->+  ZipArchive ()+addEntry t b s = addPending (I.StrictEntry t b s)  -- | Stream data from the specified source to an archive entry.--sinkEntry-  :: CompressionMethod -- ^ Compression method to use-  -> Source (ResourceT IO) ByteString -- ^ Source of entry contents-  -> EntrySelector     -- ^ Name of entry to add-  -> ZipArchive ()+sinkEntry ::+  -- | The compression method to use+  CompressionMethod ->+  -- | Source of entry contents+  ConduitT () ByteString (ResourceT IO) () ->+  -- | Name of the entry to add+  EntrySelector ->+  ZipArchive () sinkEntry t src s = addPending (I.SinkEntry t src s)  -- | Load an entry from a given file.--loadEntry-  :: CompressionMethod -- ^ Compression method to use-  -> (Path Abs File -> ZipArchive EntrySelector) -- ^ How to get 'EntrySelector'-  -> Path b File       -- ^ Path to file to add-  -> ZipArchive ()-loadEntry t f path = do-  apath   <- liftIO (canonicalizePath path)-  s       <- f apath+loadEntry ::+  -- | The compression method to use+  CompressionMethod ->+  -- | Name of the entry to add+  EntrySelector ->+  -- | Path to the file to add+  FilePath ->+  ZipArchive ()+loadEntry t s path = do+  apath <- liftIO (canonicalizePath path)   modTime <- liftIO (getModificationTime path)-  let src = CB.sourceFile (toFilePath apath)+  let src = CB.sourceFile apath   addPending (I.SinkEntry t src s)   addPending (I.SetModTime modTime s) --- | Copy an entry “as is” from another zip archive. If the entry does not--- exist in that archive, 'EntryDoesNotExist' will be eventually thrown.+#ifndef mingw32_HOST_OS+  status <- liftIO $ Unix.getFileStatus path+  setExternalFileAttrs (Unix.fromFileMode (Unix.fileMode status)) s+#endif -copyEntry-  :: Path b File       -- ^ Path to archive to copy from-  -> EntrySelector     -- ^ Name of entry (in source archive) to copy-  -> EntrySelector     -- ^ Name of entry to insert (in actual archive)-  -> ZipArchive ()+-- | Copy an entry “as is” from another zip archive. If the entry does not+-- exist in that archive, 'EntryDoesNotExist' will be thrown.+copyEntry ::+  -- | Path to the archive to copy from+  FilePath ->+  -- | Name of the entry (in the source archive) to copy+  EntrySelector ->+  -- | Name of the entry to insert (in current archive)+  EntrySelector ->+  ZipArchive () copyEntry path s' s = do   apath <- liftIO (canonicalizePath path)   addPending (I.CopyEntry apath s' s) --- | Add an entire directory to the archive. Please note that due to the--- design of the library, empty sub-directories won't be added.+-- | Add an directory to the archive. Please note that due to the design of+-- the library, empty sub-directories will not be added. ----- The action can throw the same exceptions as 'listDirRecur' and--- 'InvalidEntrySelector'.+-- The action can throw 'InvalidEntrySelector'.+packDirRecur ::+  -- | The compression method to use+  CompressionMethod ->+  -- | How to get the 'EntrySelector' from a path relative to the root of+  -- the directory we pack+  (FilePath -> ZipArchive EntrySelector) ->+  -- | Path to the directory to add+  FilePath ->+  ZipArchive ()+packDirRecur t f = packDirRecur' t f (const $ return ()) -packDirRecur-  :: CompressionMethod -- ^ Compression method to use-  -> (Path Abs File -> ZipArchive EntrySelector) -- ^ How to get 'EntrySelector'-  -> Path b Dir        -- ^ Path to directory to add-  -> ZipArchive ()-packDirRecur t f path = do-  files <- snd <$> liftIO (listDirRecur path)-  mapM_ (loadEntry t f) files+-- | The same as 'packDirRecur' but allows us to perform modifying actions+-- on the created entities as we go.+--+-- @since 1.5.0+packDirRecur' ::+  -- | The compression method to use+  CompressionMethod ->+  -- | How to get the 'EntrySelector' from a path relative to the root of+  -- the directory we pack+  (FilePath -> ZipArchive EntrySelector) ->+  -- | How to modify an entry after creation+  (EntrySelector -> ZipArchive ()) ->+  -- | Path to the directory to add+  FilePath ->+  ZipArchive ()+packDirRecur' t f patch path = do+  files <- liftIO (listDirRecur path)+  forM_ files $ \x -> do+    s <- f x+    loadEntry t s (path </> x)+    patch s  -- | Rename an entry in the archive. If the entry does not exist, nothing -- will happen.--renameEntry-  :: EntrySelector     -- ^ Original entry name-  -> EntrySelector     -- ^ New entry name-  -> ZipArchive ()+renameEntry ::+  -- | The original entry name+  EntrySelector ->+  -- | The new entry name+  EntrySelector ->+  ZipArchive () renameEntry old new = addPending (I.RenameEntry old new)  -- | Delete an entry from the archive, if it does not exist, nothing will -- happen.- deleteEntry :: EntrySelector -> ZipArchive () deleteEntry s = addPending (I.DeleteEntry s)  -- | Change compression method of an entry, if it does not exist, nothing -- will happen.--recompress-  :: CompressionMethod -- ^ New compression method-  -> EntrySelector     -- ^ Name of entry to re-compress-  -> ZipArchive ()+recompress ::+  -- | The new compression method+  CompressionMethod ->+  -- | Name of the entry to re-compress+  EntrySelector ->+  ZipArchive () recompress t s = addPending (I.Recompress t s)  -- | Set an entry comment, if that entry does not exist, nothing will -- happen. Note that if binary representation of the comment is longer than -- 65535 bytes, it will be truncated on writing.--setEntryComment-  :: Text              -- ^ Text of the comment-  -> EntrySelector     -- ^ Name of entry to comment on-  -> ZipArchive ()+setEntryComment ::+  -- | Text of the comment+  Text ->+  -- | Name of the entry to comment on+  EntrySelector ->+  ZipArchive () setEntryComment text s = addPending (I.SetEntryComment text s)  -- | Delete an entry's comment, if that entry does not exist, nothing will -- happen.- deleteEntryComment :: EntrySelector -> ZipArchive () deleteEntryComment s = addPending (I.DeleteEntryComment s) --- | Set the “last modification” date\/time. The specified entry may be+-- | Set the last modification date\/time. The specified entry may be -- missing, in that case the action has no effect.--setModTime-  :: UTCTime           -- ^ New modification time-  -> EntrySelector     -- ^ Name of entry to modify-  -> ZipArchive ()+setModTime ::+  -- | New modification time+  UTCTime ->+  -- | Name of the entry to modify+  EntrySelector ->+  ZipArchive () setModTime time s = addPending (I.SetModTime time s)  -- | Add an extra field. The specified entry may be missing, in that case -- this action has no effect.--addExtraField-  :: Word16            -- ^ Tag (header id) of extra field to add-  -> ByteString        -- ^ Body of the field-  -> EntrySelector     -- ^ Name of entry to modify-  -> ZipArchive ()+addExtraField ::+  -- | Tag (header id) of the extra field to add+  Word16 ->+  -- | Body of the field+  ByteString ->+  -- | Name of the entry to modify+  EntrySelector ->+  ZipArchive () addExtraField n b s = addPending (I.AddExtraField n b s)  -- | Delete an extra field by its type (tag). The specified entry may be -- missing, in that case this action has no effect.--deleteExtraField-  :: Word16            -- ^ Tag (header id) of extra field to delete-  -> EntrySelector     -- ^ Name of entry to modify-  -> ZipArchive ()+deleteExtraField ::+  -- | Tag (header id) of the extra field to delete+  Word16 ->+  -- | Name of the entry to modify+  EntrySelector ->+  ZipArchive () deleteExtraField n s = addPending (I.DeleteExtraField n s) --- | Perform an action on every entry in the archive.+-- | Set external file attributes. This function can be used to set file+-- permissions.+--+-- See also: "Codec.Archive.Zip.Unix".+--+-- @since 1.2.0+setExternalFileAttrs ::+  -- | External file attributes+  Word32 ->+  -- | Name of the entry to modify+  EntrySelector ->+  ZipArchive ()+setExternalFileAttrs attrs s =+  addPending (I.SetExternalFileAttributes attrs s) -forEntries-  :: (EntrySelector -> ZipArchive ()) -- ^ Action to perform-  -> ZipArchive ()+-- | Perform an action on every entry in the archive.+forEntries ::+  -- | The action to perform+  (EntrySelector -> ZipArchive ()) ->+  ZipArchive () forEntries action = getEntries >>= mapM_ action . M.keysSet --- | Set comment of the entire archive.-+-- | Set the comment of the entire archive. setArchiveComment :: Text -> ZipArchive () setArchiveComment text = addPending (I.SetArchiveComment text) --- | Delete the archive comment if it's present.-+-- | Delete the archive's comment if it's present. deleteArchiveComment :: ZipArchive () deleteArchiveComment = addPending I.DeleteArchiveComment --- | Undo changes to a specific archive entry.-+-- | Undo the changes to a specific archive entry. undoEntryChanges :: EntrySelector -> ZipArchive () undoEntryChanges s = modifyActions f-  where f = S.filter ((/= Just s) . I.targetEntry)---- | Undo changes to the archive as a whole (archive's comment).+  where+    f = S.filter ((/= Just s) . I.targetEntry) +-- | Undo the changes to the archive as a whole (archive's comment). undoArchiveChanges :: ZipArchive () undoArchiveChanges = modifyActions f-  where f = S.filter ((/= Nothing) . I.targetEntry)+  where+    f = S.filter ((/= Nothing) . I.targetEntry)  -- | Undo all changes made in this editing session.- undoAll :: ZipArchive () undoAll = modifyActions (const S.empty)  -- | Archive contents are not modified instantly, but instead changes are -- collected as “pending actions” that should be committed, in order to -- efficiently modify the archive in one pass. The actions are committed--- automatically when the program leaves the realm of 'ZipArchive' monad--- (i.e. as part of 'createArchive' or 'withArchive'), or can be forced--- explicitly with the help of this function. Once committed, changes take--- place in the file system and cannot be undone.-+-- automatically when the program leaves the 'ZipArchive' monad (i.e. as+-- part of 'createArchive' or 'withArchive'), or can be forced explicitly+-- with the help of this function. Once committed, changes take place in the+-- file system and cannot be undone. commit :: ZipArchive () commit = do-  file     <- getFilePath-  odesc    <- getArchiveDescription+  file <- getFilePath+  odesc <- getArchiveDescription   oentries <- getEntries-  actions  <- getPending-  exists   <- doesFileExist file+  actions <- getPending+  exists <- liftIO (doesFileExist file)   unless (S.null actions && exists) $ do     liftIO (I.commit file odesc oentries actions)-    -- NOTE The most robust way to update internal description of the+    -- NOTE The most robust way to update the internal description of the     -- archive is to scan it again—manual manipulations with descriptions of     -- entries are too error-prone. We also want to erase all pending     -- actions because 'I.commit' executes them all by definition.     (ndesc, nentries) <- liftIO (I.scanArchive file)-    ZipArchive . modify $ \st -> st-      { zsEntries = nentries-      , zsArchive = ndesc-      , zsActions = S.empty }+    ZipArchive . modify $ \st ->+      st+        { zsEntries = nentries,+          zsArchive = ndesc,+          zsActions = S.empty+        }  ---------------------------------------------------------------------------- -- Helpers  -- | Get the path of the actual archive file from inside of 'ZipArchive' -- monad.--getFilePath :: ZipArchive (Path Abs File)+getFilePath :: ZipArchive FilePath getFilePath = ZipArchive (gets zsFilePath)  -- | Get the collection of pending actions.- getPending :: ZipArchive (Seq I.PendingAction) getPending = ZipArchive (gets zsActions) --- | Modify the collection of pending actions in some way.-+-- | Modify the collection of pending actions. modifyActions :: (Seq I.PendingAction -> Seq I.PendingAction) -> ZipArchive () modifyActions f = ZipArchive (modify g)-  where g st = st { zsActions = f (zsActions st) }+  where+    g st = st {zsActions = f (zsActions st)}  -- | Add a new action to the list of pending actions.- addPending :: I.PendingAction -> ZipArchive () addPending a = modifyActions (|> a)++-- | Recursively list a directory. Do not return paths to empty directories.+listDirRecur :: FilePath -> IO [FilePath]+listDirRecur path = DList.toList <$> go ""+  where+    go adir = do+      let cdir = path </> adir+      raw <- listDirectory cdir+      fmap mconcat . forM raw $ \case+        "" -> return mempty+        "." -> return mempty+        ".." -> return mempty+        x -> do+          let fullx = cdir </> x+              adir' = adir </> x+          isFile <- doesFileExist fullx+          isDir <- doesDirectoryExist fullx+          if isFile+            then return (DList.singleton adir')+            else+              if isDir+                then go adir'+                else return mempty++-- | Perform an action ignoring IO exceptions it may throw.+ignoringAbsence :: IO () -> IO ()+ignoringAbsence io = catchJust select io handler+  where+    select e = if isDoesNotExistError e then Just e else Nothing+    handler = const (return ())
Codec/Archive/Zip/CP437.hs view
@@ -1,6 +1,6 @@ -- | -- Module      :  Codec.Archive.Zip.CP437--- Copyright   :  © 2016–2017 Mark Karpov+-- Copyright   :  © 2016–present Mark Karpov -- License     :  BSD 3 clause -- -- Maintainer  :  Mark Karpov <markkarpov92@gmail.com>@@ -8,29 +8,28 @@ -- Portability :  portable -- -- Support for decoding of CP 437 text.- module Codec.Archive.Zip.CP437-  ( decodeCP437 )+  ( decodeCP437,+  ) where  import Control.Arrow (first) import Data.ByteString (ByteString)+import Data.ByteString qualified as B import Data.Char import Data.Text (Text)+import Data.Text qualified as T import Data.Word (Word8)-import qualified Data.ByteString as B-import qualified Data.Text       as T  -- | Decode a 'ByteString' containing CP 437 encoded text.- decodeCP437 :: ByteString -> Text-decodeCP437 bs = T.unfoldrN-  (B.length bs)-  (fmap (first decodeByteCP437) . B.uncons)-  bs+decodeCP437 bs =+  T.unfoldrN+    (B.length bs)+    (fmap (first decodeByteCP437) . B.uncons)+    bs  -- | Decode a single byte of CP437 encoded text.- decodeByteCP437 :: Word8 -> Char decodeByteCP437 byte = chr $ case byte of   128 -> 199@@ -161,4 +160,4 @@   253 -> 178   254 -> 9632   255 -> 160-  x   -> fromIntegral x -- the rest of characters translate directly+  x -> fromIntegral x -- the rest of characters translate directly
Codec/Archive/Zip/Internal.hs view
@@ -1,1018 +1,1256 @@--- |--- Module      :  Codec.Archive.Zip.Internal--- Copyright   :  © 2016–2017 Mark Karpov--- License     :  BSD 3 clause------ Maintainer  :  Mark Karpov <markkarpov92@gmail.com>--- Stability   :  experimental--- Portability :  portable------ Low-level, non-public concepts and operations.--{-# LANGUAGE ScopedTypeVariables #-}--module Codec.Archive.Zip.Internal-  ( PendingAction (..)-  , targetEntry-  , scanArchive-  , sourceEntry-  , crc32Sink-  , commit )-where--import Codec.Archive.Zip.CP437 (decodeCP437)-import Codec.Archive.Zip.Type-import Control.Applicative (many, (<|>))-import Control.Monad-import Control.Monad.Catch-import Control.Monad.Trans.Maybe-import Control.Monad.Trans.Resource (ResourceT, runResourceT, MonadResource)-import Data.Bits-import Data.Bool (bool)-import Data.ByteString (ByteString)-import Data.Char (ord)-import Data.Conduit (Conduit, Source, Sink, (=$=), ($$), awaitForever, yield)-import Data.Conduit.Internal (zipSinks)-import Data.Digest.CRC32 (crc32Update)-import Data.Fixed (Fixed (..))-import Data.Foldable (foldl')-import Data.Map.Strict (Map, (!))-import Data.Maybe (fromJust, catMaybes, isNothing)-import Data.Monoid ((<>))-import Data.Sequence (Seq, (><), (|>))-import Data.Serialize-import Data.Text (Text)-import Data.Time-import Data.Version-import Data.Word (Word16, Word32)-import Numeric.Natural (Natural)-import Path-import System.IO-import System.PlanB-import qualified Data.ByteString     as B-import qualified Data.Conduit.BZlib  as BZ-import qualified Data.Conduit.Binary as CB-import qualified Data.Conduit.List   as CL-import qualified Data.Conduit.Zlib   as Z-import qualified Data.Map.Strict     as M-import qualified Data.Sequence       as S-import qualified Data.Set            as E-import qualified Data.Text           as T-import qualified Data.Text.Encoding  as T--------------------------------------------------------------------------------- Data types---- | The sum type describes all possible actions that can be performed on--- archive.--data PendingAction-  = SinkEntry CompressionMethod (Source (ResourceT IO) ByteString) EntrySelector-    -- ^ Add entry given its 'Source'-  | CopyEntry (Path Abs File) EntrySelector EntrySelector-    -- ^ Copy an entry form another archive without re-compression-  | RenameEntry EntrySelector EntrySelector-    -- ^ Change name the entry inside archive-  | DeleteEntry EntrySelector-    -- ^ Delete entry from archive-  | Recompress CompressionMethod EntrySelector-    -- ^ Change compression method on an entry-  | SetEntryComment Text EntrySelector-    -- ^ Set comment for a particular entry-  | DeleteEntryComment EntrySelector-    -- ^ Delete comment of particular entry-  | SetModTime UTCTime EntrySelector-    -- ^ Set modification time of particular entry-  | AddExtraField Word16 ByteString EntrySelector-    -- ^ Add an extra field to specified entry-  | DeleteExtraField Word16 EntrySelector-    -- ^ Delete an extra filed of specified entry-  | SetArchiveComment Text-    -- ^ Set comment for entire archive-  | DeleteArchiveComment-    -- ^ Delete comment of entire archive---- | Collection of maps describing how to produce entries in resulting--- archive.--data ProducingActions = ProducingActions-  { paCopyEntry :: Map (Path Abs File) (Map EntrySelector EntrySelector)-  , paSinkEntry :: Map EntrySelector (Source (ResourceT IO) ByteString) }---- | Collection of editing actions, that is, actions that modify already--- existing entries.--data EditingActions = EditingActions-  { eaCompression   :: Map EntrySelector CompressionMethod-  , eaEntryComment  :: Map EntrySelector Text-  , eaDeleteComment :: Map EntrySelector ()-  , eaModTime       :: Map EntrySelector UTCTime-  , eaExtraField    :: Map EntrySelector (Map Word16 ByteString)-  , eaDeleteField   :: Map EntrySelector (Map Word16 ()) }---- | Origin of entries that can be streamed into archive.--data EntryOrigin-  = GenericOrigin-  | Borrowed EntryDescription---- | Type of file header: local or central directory.--data HeaderType-  = LocalHeader-  | CentralDirHeader-  deriving Eq---- | Data descriptor representation.--data DataDescriptor = DataDescriptor-  { ddCRC32            :: Word32-  , ddCompressedSize   :: Natural-  , ddUncompressedSize :: Natural }---- | A temporary data structure to hold Zip64 extra data field information.--data Zip64ExtraField = Zip64ExtraField-  { z64efUncompressedSize :: Natural-  , z64efCompressedSize   :: Natural-  , z64efOffset           :: Natural }---- | MS-DOS date-time: a pair of 'Word16' (date, time) with the following--- structure:------ > DATE bit     0 - 4           5 - 8           9 - 15--- >      value   day (1 - 31)    month (1 - 12)  years from 1980--- > TIME bit     0 - 4           5 - 10          11 - 15--- >      value   seconds*        minute          hour--- >              *stored in two-second increments--data MsDosTime = MsDosTime-  { msDosDate :: Word16-  , msDosTime :: Word16 }--------------------------------------------------------------------------------- Constants---- | “Version created by” to specify when writing archive data.--zipVersion :: Version-zipVersion = Version [4,6] []--------------------------------------------------------------------------------- Higher-level operations---- | Scan the central directory of an archive and return its description--- 'ArchiveDescription' as well as a collection of its entries.------ This operation may fail with:------     * @isAlreadyInUseError@ if the file is already open and cannot be---     reopened;------     * @isDoesNotExistError@ if the file does not exist;------     * @isPermissionError@ if the user does not have permission to open---     the file;------     * 'ParsingFailed' when specified archive is something this library---     cannot parse (this includes multi-disk archives, for example).------ Please note that entries with invalid (non-portable) file names may be--- missing in the list of entries. Files that are compressed with--- unsupported compression methods are skipped as well. Also, if several--- entries would collide on some operating systems (such as Windows, because--- of its case-insensitivity), only one of them will be available, because--- 'EntrySelector' is case-insensitive. These are the consequences of the--- design decision to make it impossible to create non-portable archives--- with this library.--scanArchive-  :: Path Abs File     -- ^ Path to archive to scan-  -> IO (ArchiveDescription, Map EntrySelector EntryDescription)-scanArchive path = withBinaryFile (toFilePath path) ReadMode $ \h -> do-  mecdOffset <- locateECD path h-  case mecdOffset of-    Just ecdOffset -> do-      hSeek h AbsoluteSeek ecdOffset-      ecdSize <- subtract ecdOffset <$> hFileSize h-      ecdRaw  <- B.hGet h (fromIntegral ecdSize)-      case runGet getECD ecdRaw of-        Left  msg -> throwM (ParsingFailed path msg)-        Right ecd -> do-          hSeek h AbsoluteSeek $ fromIntegral (adCDOffset ecd)-          cdRaw <- B.hGet h $ fromIntegral (adCDSize ecd)-          case runGet getCD cdRaw of-            Left  msg -> throwM (ParsingFailed path msg)-            Right cd  -> return (ecd, cd)-    Nothing ->-      throwM (ParsingFailed path "Cannot locate end of central directory")---- | Given location of archive and information about specific archive entry--- 'EntryDescription', return 'Source' of its data. Actual data can be--- compressed or uncompressed depending on the third argument.--sourceEntry-  :: MonadResource m-  => Path Abs File     -- ^ Path to archive that contains the entry-  -> EntryDescription  -- ^ Information needed to extract entry of interest-  -> Bool              -- ^ Should we stream uncompressed data?-  -> Source m ByteString -- ^ Source of uncompressed data-sourceEntry path EntryDescription {..} d =-  source =$= CB.isolate (fromIntegral edCompressedSize) =$= decompress-  where-    source = CB.sourceIOHandle $ do-      h <- openFile (toFilePath 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-      else awaitForever yield---- | Undertake /all/ actions specified as the fourth argument of the--- function. This transforms given pending actions so they can be performed--- in one pass, and then they are performed in the most efficient way.--commit-  :: Path Abs File     -- ^ Location of archive file to edit or create-  -> ArchiveDescription -- ^ Archive description-  -> Map EntrySelector EntryDescription -- ^ Current list of entires-  -> Seq PendingAction -- ^ Collection of pending actions-  -> IO ()-commit path ArchiveDescription {..} entries xs =-  withNewFile (overrideIfExists      <>-               nameTemplate ".zip"   <>-               tempDir (parent path) <>-               moveByRenaming) path $ \temp -> do-    let (ProducingActions coping sinking, editing) =-          optimize (toRecreatingActions path entries >< xs)-        comment = predictComment adComment xs-    withBinaryFile (toFilePath temp) WriteMode $ \h -> do-      copiedCD <- M.unions <$> forM (M.keys coping) (\srcPath ->-        copyEntries h srcPath (coping ! srcPath) editing)-      let sinkingKeys = M.keys $ sinking `M.difference` copiedCD-      sunkCD   <- M.fromList <$> forM sinkingKeys (\selector ->-        sinkEntry h selector GenericOrigin (sinking ! selector) editing)-      writeCD h comment (copiedCD `M.union` sunkCD)---- | Determine what comment in new archive will look like given its original--- value and a collection of pending actions.--predictComment :: Maybe Text -> Seq PendingAction -> Maybe Text-predictComment original xs =-  case S.index xs <$> S.findIndexR (isNothing . targetEntry) xs of-    Nothing                      -> original-    Just DeleteArchiveComment    -> Nothing-    Just (SetArchiveComment txt) -> Just txt-    Just _                       -> Nothing---- | Transform a map representing existing entries into a collection of--- actions that re-create those entires.--toRecreatingActions-  :: Path Abs File     -- ^ Name of the archive file where entires are found-  -> Map EntrySelector EntryDescription -- ^ Actual list of entires-  -> Seq PendingAction -- ^ Actions that recreate the archive entries-toRecreatingActions path entries = E.foldl' f S.empty (M.keysSet entries)-  where-    f s e = s |> CopyEntry path e e---- | Transform a collection of 'PendingAction's into 'ProducingActions' and--- 'EditingActions'—data that describes how to create resulting archive.--optimize-  :: Seq PendingAction -- ^ Collection of pending actions-  -> (ProducingActions, EditingActions) -- ^ Optimized data-optimize = foldl' f-  ( ProducingActions M.empty M.empty-  , EditingActions   M.empty M.empty M.empty M.empty M.empty M.empty )-  where-    f (pa, ea) a = case a of-      SinkEntry m src s ->-        ( pa { paSinkEntry   = M.insert s src (paSinkEntry pa)-             , paCopyEntry   = M.map (M.filter (/= s)) (paCopyEntry pa) }-        , (clearEditingFor s ea)-             { eaCompression = M.insert s m (eaCompression ea) } )-      CopyEntry path os ns ->-        ( pa { paSinkEntry = M.delete ns (paSinkEntry pa)-             , paCopyEntry = M.alter (ef os ns) path (paCopyEntry pa) }-        , clearEditingFor ns ea )-      RenameEntry os ns ->-        ( pa { paCopyEntry = M.map (M.map $ re os ns) (paCopyEntry pa)-             , paSinkEntry = renameKey os ns (paSinkEntry pa) }-        , ea { eaCompression   = renameKey os ns (eaCompression ea)-             , eaEntryComment  = renameKey os ns (eaEntryComment ea)-             , eaDeleteComment = renameKey os ns (eaDeleteComment ea)-             , eaModTime       = renameKey os ns (eaModTime ea)-             , eaExtraField    = renameKey os ns (eaExtraField ea)-             , eaDeleteField   = renameKey os ns (eaDeleteField ea) } )-      DeleteEntry s ->-        ( pa { paSinkEntry = M.delete s (paSinkEntry pa)-             , paCopyEntry = M.map (M.delete s) (paCopyEntry pa) }-        , clearEditingFor s ea )-      Recompress m s ->-        (pa, ea { eaCompression = M.insert s m (eaCompression ea) })-      SetEntryComment txt s ->-        ( pa-        , ea { eaEntryComment  = M.insert s txt (eaEntryComment ea)-             , eaDeleteComment = M.delete s (eaDeleteComment ea) } )-      DeleteEntryComment s ->-        ( pa-        , ea { eaEntryComment  = M.delete s (eaEntryComment ea)-             , eaDeleteComment = M.insert s () (eaDeleteComment ea) } )-      SetModTime time s ->-        (pa, ea { eaModTime = M.insert s time (eaModTime ea) })-      AddExtraField n b s ->-        ( pa-        , ea { eaExtraField  = M.alter (ef n b) s (eaExtraField ea)-             , eaDeleteField = M.delete s (eaDeleteField ea) } )-      DeleteExtraField n s ->-        ( pa-        , ea { eaExtraField = M.alter (er n) s (eaExtraField ea)-             , eaDeleteField = M.alter (ef n ()) s (eaDeleteField ea) } )-      _ -> (pa, ea)-    clearEditingFor s ea = ea-      { eaCompression   = M.delete s (eaCompression ea)-      , eaEntryComment  = M.delete s (eaEntryComment ea)-      , eaDeleteComment = M.delete s (eaDeleteComment ea)-      , eaModTime       = M.delete s (eaModTime ea)-      , eaExtraField    = M.delete s (eaExtraField ea)-      , eaDeleteField   = M.delete s (eaDeleteField ea) }-    re o n x = if x == o then n else x-    ef k v (Just m) = Just (M.insert k v m)-    ef k v Nothing  = Just (M.singleton k v)-    er k (Just m)   = let n = M.delete k m in-      if M.null n then Nothing else Just n-    er _ Nothing    = Nothing---- | Copy entries from another archive and write them into the file--- associated with given handle. This can throw 'EntryDoesNotExist' if there--- is no such entry in that archive.--copyEntries-  :: Handle            -- ^ Opened 'Handle' of zip archive file-  -> Path Abs File     -- ^ Path to file from which to copy the entries-  -> Map EntrySelector EntrySelector-     -- ^ 'Map' from original name to name to use in new archive-  -> EditingActions    -- ^ Additional info that can influence result-  -> IO (Map EntrySelector EntryDescription)-     -- ^ Info to generate central directory file headers later-copyEntries h path m e = 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)---- | Sink entry from given stream into the file associated with given--- 'Handle'.--sinkEntry-  :: Handle            -- ^ Opened 'Handle' of zip archive file-  -> EntrySelector     -- ^ Name of entry to add-  -> EntryOrigin       -- ^ Origin of entry (can contain additional info)-  -> Source (ResourceT IO) ByteString -- ^ Source of entry contents-  -> EditingActions    -- ^ Additional info that can influence result-  -> IO (EntrySelector, EntryDescription)-     -- ^ Info to generate central directory file headers later-sinkEntry h s o src EditingActions {..} = do-  currentTime <- getCurrentTime-  offset  <- hTell h-  let compressed = case o of-        GenericOrigin -> Store-        Borrowed ed -> edCompression ed-      compression = M.findWithDefault compressed s eaCompression-      recompression = compression /= compressed-      modTime = case o of-        GenericOrigin -> currentTime-        Borrowed ed -> edModTime ed-      oldExtraFields = case o of-        GenericOrigin -> M.empty-        Borrowed ed -> edExtraField ed-      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-      desc0 = EntryDescription -- to write in local header-        { edVersionMadeBy    = zipVersion-        , edVersionNeeded    = zipVersion-        , edCompression      = compression-        , edModTime          = M.findWithDefault modTime s eaModTime-        , edCRC32            = 0 -- to be overwritten after streaming-        , edCompressedSize   = 0 -- ↑-        , edUncompressedSize = 0 -- ↑-        , edOffset           = fromIntegral offset-        , edComment          = M.lookup s eaEntryComment <|> oldComment-        , edExtraField       = extraField }-  B.hPut h (runPut (putHeader LocalHeader s desc0))-  DataDescriptor {..} <- runResourceT $-    if recompression-      then-        if compressed == Store-          then src $$ sinkData h compression-          else src =$= decompressingPipe compressed $$ sinkData h compression-      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            =-              bool (edCRC32 ed) ddCRC32 recompression-          , edCompressedSize   =-              bool (edCompressedSize ed) ddCompressedSize recompression-          , edUncompressedSize =-              bool (edUncompressedSize ed) ddUncompressedSize recompression }-      desc2 = desc1-        { edVersionNeeded =-          getZipVersion (needsZip64 desc1) (Just compression) }-  hSeek h AbsoluteSeek offset-  B.hPut h (runPut (putHeader LocalHeader s desc2))-  hSeek h AbsoluteSeek afterStreaming-  return (s, desc2)---- | Create 'Sink' to stream data there. Once streaming is finished, return--- 'DataDescriptor' for the streamed data. The action /does not/ close given--- 'Handle'.--sinkData-  :: Handle            -- ^ Opened 'Handle' of zip archive file-  -> CompressionMethod -- ^ Compression method to apply-  -> Sink ByteString (ResourceT IO) DataDescriptor-     -- ^ 'Sink' where to stream data-sinkData h compression = do-  let sizeSink  = CL.fold (\acc input -> fromIntegral (B.length input) + acc) 0-      dataSink  = fst <$> zipSinks sizeSink (CB.sinkHandle h)-      withCompression = zipSinks (zipSinks sizeSink crc32Sink)-  ((uncompressedSize, crc32), compressedSize) <--    case compression of-      Store   -> withCompression-        dataSink-      Deflate -> withCompression $-        Z.compress 9 (Z.WindowBits (-15)) =$= dataSink-      BZip2   -> withCompression $-        BZ.bzip2 =$= dataSink-  return DataDescriptor-    { ddCRC32            = fromIntegral crc32-    , ddCompressedSize   = compressedSize-    , ddUncompressedSize = uncompressedSize }---- | Append central directory entries and end of central directory record to--- the file that given 'Handle' is associated with. Note that this--- automatically writes Zip64 end of central directory record and Zip64 end--- of central directory locator when necessary.--writeCD-  :: Handle            -- ^ Opened handle of zip archive file-  -> Maybe Text        -- ^ Commentary to entire archive-  -> Map EntrySelector EntryDescription-  -- ^ Info about already written local headers and entry data-  -> IO ()-writeCD h comment m = do-  let cd = runPut (putCD m)-  cdOffset <- fromIntegral <$> hTell h-  B.hPut h cd -- write central directory-  let totalCount = fromIntegral (M.size m)-      cdSize     = fromIntegral (B.length cd)-      needZip64  =-        totalCount  >= ffff-        || cdSize   >= ffffffff-        || cdOffset >= ffffffff-  when needZip64 $ do-    zip64ecdOffset <- fromIntegral <$> hTell h-    (B.hPut h . runPut) (putZip64ECD totalCount cdSize cdOffset)-    (B.hPut h . runPut) (putZip64ECDLocator zip64ecdOffset)-  (B.hPut h . runPut) (putECD totalCount cdSize cdOffset comment)--------------------------------------------------------------------------------- Binary serialization---- | Extract the number of bytes between start of file name in local header--- and start of actual data.--getLocalHeaderGap :: Get Integer-getLocalHeaderGap = do-  getSignature 0x04034b50-  skip 2 -- version needed to extract-  skip 2 -- general purpose bit flag-  skip 2 -- compression method-  skip 2 -- last mod file time-  skip 2 -- last mod file date-  skip 4 -- crc-32 check sum-  skip 4 -- compressed size-  skip 4 -- uncompressed size-  fileNameSize   <- fromIntegral <$> getWord16le -- file name length-  extraFieldSize <- fromIntegral <$> getWord16le -- extra field length-  return (fileNameSize + extraFieldSize)---- | Parse central directory file headers and put them into 'Map'.--getCD :: Get (Map EntrySelector EntryDescription)-getCD = M.fromList . catMaybes <$> many getCDHeader---- | Parse a single central directory file header. If it's a directory or--- file compressed with unsupported compression method, 'Nothing' is--- returned.--getCDHeader :: Get (Maybe (EntrySelector, EntryDescription))-getCDHeader = do-  getSignature 0x02014b50 -- central file header signature-  versionMadeBy  <- toVersion <$> getWord16le -- version made by-  versionNeeded  <- toVersion <$> getWord16le -- version needed to extract-  when (versionNeeded > zipVersion) . fail $-    "Version required to extract the archive is "-    ++ showVersion versionNeeded ++ " (can do "-    ++ showVersion zipVersion ++ ")"-  bitFlag        <- getWord16le -- general purpose bit flag-  when (any (testBit bitFlag) [0,6,13]) . fail $-    "Encrypted archives are not supported"-  let needUnicode = testBit bitFlag 11-  mcompression   <- toCompressionMethod <$> getWord16le -- compression method-  modTime        <- getWord16le -- last mod file time-  modDate        <- getWord16le -- last mod file date-  crc32          <- getWord32le -- CRC32 check sum-  compressed     <- fromIntegral <$> getWord32le -- compressed size-  uncompressed   <- fromIntegral <$> getWord32le -- uncompressed size-  fileNameSize   <- getWord16le -- file name length-  extraFieldSize <- getWord16le -- extra field length-  commentSize    <- getWord16le -- file comment size-  skip 8 -- disk number start, internal/external file attributes-  offset         <- fromIntegral <$> getWord32le -- offset of local header-  fileName       <- decodeText needUnicode <$>-    getBytes (fromIntegral fileNameSize) -- file name-  extraField     <- M.fromList <$>-    isolate (fromIntegral extraFieldSize) (many getExtraField)-  -- ↑ extra fields in their raw form-  comment <- decodeText needUnicode <$> getBytes (fromIntegral commentSize)-  -- ↑ file comment-  let dfltZip64 = Zip64ExtraField-        { z64efUncompressedSize = uncompressed-        , z64efCompressedSize   = compressed-        , z64efOffset           = offset }-      z64ef = case M.lookup 1 extraField of-        Nothing -> dfltZip64-        Just b  -> parseZip64ExtraField dfltZip64 b-  case mcompression of-    Nothing -> return Nothing-    Just compression ->-      let desc = EntryDescription-            { edVersionMadeBy    = versionMadeBy-            , edVersionNeeded    = versionNeeded-            , edCompression      = compression-            , edModTime          = fromMsDosTime (MsDosTime modDate modTime)-            , edCRC32            = crc32-            , edCompressedSize   = z64efCompressedSize   z64ef-            , edUncompressedSize = z64efUncompressedSize z64ef-            , edOffset           = z64efOffset           z64ef-            , edComment = if commentSize == 0 then Nothing else comment-            , edExtraField       = extraField }-      in return $ (,desc) <$>-         (fileName >>= parseRelFile . T.unpack >>= mkEntrySelector)---- | Parse an extra-field.--getExtraField :: Get (Word16, ByteString)-getExtraField = do-  header <- getWord16le -- header id-  size   <- getWord16le -- data size-  body   <- getBytes (fromIntegral size) -- content-  return (header, body)---- | Get signature. If the extracted data is not equal to provided--- signature, fail.--getSignature :: Word32 -> Get ()-getSignature sig = do-  x <- getWord32le -- grab 4-byte signature-  unless (x == sig) . fail $-    "Expected signature " ++ show sig ++ ", but got: " ++ show x---- | Parse 'Zip64ExtraField' from its binary representation.--parseZip64ExtraField-  :: Zip64ExtraField   -- ^ What is read from central directory file header-  -> ByteString        -- ^ Actual binary representation-  -> Zip64ExtraField   -- ^ Result-parseZip64ExtraField dflt@Zip64ExtraField {..} b =-  either (const dflt) id . flip runGet b $ do-    let ifsat v = if v >= ffffffff-          then fromIntegral <$> getWord64le-          else return v-    uncompressed <- ifsat z64efUncompressedSize -- uncompressed size-    compressed   <- ifsat z64efCompressedSize -- compressed size-    offset       <- ifsat z64efOffset -- offset of local file header-    return (Zip64ExtraField uncompressed compressed offset)---- | Produce binary representation of 'Zip64ExtraField'.--makeZip64ExtraField-  :: HeaderType        -- ^ Is this for local or central directory header?-  -> Zip64ExtraField   -- ^ Zip64 extra field's data-  -> ByteString        -- ^ Resulting representation-makeZip64ExtraField c Zip64ExtraField {..} = runPut $ do-  when (c == LocalHeader || z64efUncompressedSize >= ffffffff) $-    putWord64le (fromIntegral z64efUncompressedSize) -- uncompressed size-  when (c == LocalHeader || z64efCompressedSize >= ffffffff) $-    putWord64le (fromIntegral z64efCompressedSize) -- compressed size-  when (c == CentralDirHeader && 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)-  putWord16le headerId-  putWord16le (fromIntegral $ B.length b)-  putByteString b---- | Create 'ByteString' representing entire central directory.--putCD :: Map EntrySelector EntryDescription -> Put-putCD m = forM_ (M.keys m) $ \s ->-  putHeader CentralDirHeader s (m ! s)---- | Create 'ByteString' representing local file header if the first--- argument is 'False' and central directory file header otherwise.--putHeader-  :: HeaderType        -- ^ Type of header to generate-  -> EntrySelector     -- ^ Name of entry to write-  -> EntryDescription  -- ^ Description of entry-  -> Put-putHeader c' s EntryDescription {..} = do-  let c = c' == CentralDirHeader-  putWord32le (bool 0x04034b50 0x02014b50 c)-  -- ↑ local/central file header signature-  when c $-    putWord16le (fromVersion edVersionMadeBy) -- version made by-  putWord16le (fromVersion edVersionNeeded) -- version needed to extract-  let entryName = getEntryName s-      rawName   = T.encodeUtf8 entryName-      comment   = B.take 0xffff (maybe B.empty T.encodeUtf8 edComment)-      unicode   = needsUnicode entryName-        || maybe False needsUnicode edComment-      modTime   = toMsDosTime edModTime-  putWord16le (if unicode then setBit 0 11 else 0)-  -- ↑ general purpose bit-flag-  putWord16le (fromCompressionMethod edCompression) -- compression method-  putWord16le (msDosTime modTime) -- last mod file time-  putWord16le (msDosDate modTime) -- last mod file date-  putWord32le edCRC32 -- CRC-32 checksum-  putWord32le (withSaturation edCompressedSize) -- compressed size-  putWord32le (withSaturation edUncompressedSize) -- uncompressed size-  putWord16le (fromIntegral $ B.length rawName) -- file name length-  let zip64ef = makeZip64ExtraField c' Zip64ExtraField-        { z64efUncompressedSize = edUncompressedSize-        , z64efCompressedSize   = edCompressedSize-        , z64efOffset           = edOffset }-      extraField = B.take 0xffff . runPut . putExtraField $-        M.insert 1 zip64ef edExtraField-  putWord16le (fromIntegral $ B.length extraField) -- extra field length-  when c $ do-    putWord16le (fromIntegral $ B.length comment) -- file comment length-    putWord16le 0 -- disk number start-    putWord16le 0 -- internal file attributes-    putWord32le 0 -- external file attributes-    putWord32le (withSaturation edOffset) -- relative offset of local header-  putByteString rawName -- file name (variable size)-  putByteString extraField -- extra field (variable size)-  when c (putByteString comment) -- file comment (variable size)---- | Create 'ByteString' representing Zip64 end of central directory record.--putZip64ECD-  :: Natural           -- ^ Total number of entries-  -> Natural           -- ^ Size of the central directory-  -> Natural           -- ^ Offset of central directory record-  -> Put-putZip64ECD totalCount cdSize cdOffset = do-  putWord32le 0x06064b50 -- zip64 end of central dir signature-  putWord64le 44 -- size of zip64 end of central dir record-  putWord16le (fromVersion zipVersion) -- version made by-  putWord16le (fromVersion $ getZipVersion True Nothing)-  -- ↑ version needed to extract-  putWord32le 0 -- number of this disk-  putWord32le 0 -- number of the disk with the start of the central directory-  putWord64le (fromIntegral totalCount) -- total number of entries (this disk)-  putWord64le (fromIntegral totalCount) -- total number of entries-  putWord64le (fromIntegral cdSize) -- size of the central directory-  putWord64le (fromIntegral cdOffset) -- offset of central directory---- | Create 'ByteString' representing Zip64 end of central directory--- locator.--putZip64ECDLocator-  :: Natural           -- ^ Offset of Zip64 end of central directory-  -> Put-putZip64ECDLocator ecdOffset = do-  putWord32le 0x07064b50 -- zip64 end of central dir locator signature-  putWord32le 0 -- number of the disk with the start of the zip64 end of-    -- central directory-  putWord64le (fromIntegral ecdOffset) -- relative offset of the zip64 end-    -- of central directory record-  putWord32le 1 -- total number of disks---- | Parse end of central directory record or Zip64 end of central directory--- record depending on signature binary data begins with.--getECD :: Get ArchiveDescription-getECD = do-  sig <- getWord32le -- end of central directory signature-  let zip64 = sig == 0x06064b50-  unless (sig == 0x06054b50 || sig == 0x06064b50) $-    fail "Cannot locate end of central directory"-  zip64size <- if zip64 then do-    x <- getWord64le -- size of zip64 end of central directory record-    skip 2 -- version made by-    skip 2 -- version needed to extract-    return (Just x)-    else return Nothing-  thisDisk <- bool (fromIntegral <$> getWord16le) getWord32le zip64-  -- ↑ number of this disk-  cdDisk   <- bool (fromIntegral <$> getWord16le) getWord32le zip64-  -- ↑ number of the disk with the start of the central directory-  unless (thisDisk == 0 && cdDisk == 0) $-    fail "No support for multi-disk archives"-  skip (bool 2 8 zip64)-  -- ↑ total number of entries in the central directory on this disk-  skip (bool 2 8 zip64)-  -- ↑ total number of entries in the central directory-  cdSize   <- bool (fromIntegral <$> getWord32le) getWord64le zip64-  -- ↑ size of the central directory-  cdOffset <- bool (fromIntegral <$> getWord32le) getWord64le zip64-  -- ↑ offset of start of central directory with respect to the starting-  -- disk number-  when zip64 . skip . fromIntegral $ fromJust zip64size - 4 -- obviously-  commentSize <- getWord16le -- .ZIP file comment length-  comment <- decodeText True <$> getBytes (fromIntegral commentSize)-  -- ↑ archive comment, it's uncertain how we should decide on encoding here-  return ArchiveDescription-    { adComment  = if commentSize == 0 then Nothing else comment-    , adCDOffset = fromIntegral cdOffset-    , adCDSize   = fromIntegral cdSize }---- | Create 'ByteString' representing end of central directory record.--putECD-  :: Natural           -- ^ Total number of entries-  -> Natural           -- ^ Size of the central directory-  -> Natural           -- ^ Offset of central directory record-  -> Maybe Text        -- ^ Zip file comment-  -> Put-putECD totalCount cdSize cdOffset mcomment = do-  putWord32le 0x06054b50 -- end of central dir signature-  putWord16le 0 -- number of this disk-  putWord16le 0 -- number of the disk with the start of the central directory-  putWord16le (withSaturation totalCount)-  -- ↑ total number of entries on this disk-  putWord16le (withSaturation totalCount) -- total number of entries-  putWord32le (withSaturation cdSize) -- size of central directory-  putWord32le (withSaturation cdOffset) -- offset of start of central directory-  let comment = maybe B.empty T.encodeUtf8 mcomment-  putWord16le (fromIntegral $ B.length comment)-  putByteString comment---- | Find absolute offset of end of central directory record or, if present,--- Zip64 end of central directory record.--locateECD :: Path Abs File -> Handle -> IO (Maybe Integer)-locateECD path h = sizeCheck-  where--    sizeCheck = do-      fsize    <- hFileSize h-      let limit = max 0 (fsize - 0xffff - 22)-      if fsize < 22-        then return Nothing-        else hSeek h SeekFromEnd (-22) >> loop limit--    loop limit = do-      sig <- getNum getWord32le 4-      pos <- subtract 4 <$> hTell h-      let again = hSeek h AbsoluteSeek (pos - 1) >> loop limit-          done  = pos <= limit-      if sig == 0x06054b50-        then do-          result <- runMaybeT $-            MaybeT (checkComment pos) >>=-            MaybeT . checkCDSig       >>=-            MaybeT . checkZip64-          case result of-            Nothing -> bool again (return Nothing) done-            Just ecd -> return (Just ecd)-        else bool again (return Nothing) done--    checkComment pos = do-      size <- hFileSize h-      hSeek h AbsoluteSeek (pos + 20)-      l <- fromIntegral <$> getNum getWord16le 2-      return $ if l + 22 == size - pos-        then Just pos-        else Nothing--    checkCDSig pos = do-      hSeek h AbsoluteSeek (pos + 16)-      sigPos <- fromIntegral <$> getNum getWord32le 4-      if sigPos == 0xffffffff -- Zip64 is probably used-        then return (Just pos)-        else do-          hSeek h AbsoluteSeek sigPos-          cdSig  <- getNum getWord32le 4-          return $ if cdSig == 0x02014b50 ||-            -- ↑ normal case: central directory file header signature-                      cdSig == 0x06064b50 ||-            -- ↑ happens when zip 64 archive is empty-                      cdSig == 0x06054b50-            -- ↑ happens when vanilla archive is empty-            then Just pos-            else Nothing--    checkZip64 pos =-      if pos < 20-        then return (Just pos)-        else do-          hSeek h AbsoluteSeek (pos - 20)-          zip64locatorSig <- getNum getWord32le 4-          if zip64locatorSig == 0x07064b50-            then do-              hSeek h AbsoluteSeek (pos - 12)-              Just . fromIntegral <$> getNum getWord64le 8-            else return (Just pos)--    getNum f n = do-      result <- runGet f <$> B.hGet h n-      case result of-        Left msg -> throwM (ParsingFailed path msg)-        Right val -> return val--------------------------------------------------------------------------------- Helpers---- | Rename an entry (key) in a 'Map'.--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)---- | Like 'fromIntegral', but with saturation when converting to bounded--- types.--withSaturation :: forall a b. (Integral a, Integral b, Bounded b) => a -> b-withSaturation x =-  if (fromIntegral x :: Integer) > (fromIntegral bound :: Integer)-    then bound-    else fromIntegral x-  where bound = maxBound :: b---- | Determine target entry of action.--targetEntry :: PendingAction -> Maybe EntrySelector-targetEntry (SinkEntry      _ _ s) = Just s-targetEntry (CopyEntry      _ _ s) = Just s-targetEntry (RenameEntry      s _) = Just s-targetEntry (DeleteEntry        s) = Just s-targetEntry (Recompress       _ s) = Just s-targetEntry (SetEntryComment  _ s) = Just s-targetEntry (DeleteEntryComment s) = Just s-targetEntry (SetModTime       _ s) = Just s-targetEntry (AddExtraField  _ _ s) = Just s-targetEntry (DeleteExtraField _ s) = Just s-targetEntry (SetArchiveComment  _) = Nothing-targetEntry DeleteArchiveComment   = Nothing---- | Decode 'ByteString'. The first argument indicates whether we should--- treat it as UTF-8 (in case bit 11 of general-purpose bit flag is set),--- otherwise the function assumes CP437. Note that since not every stream of--- bytes constitutes valid UTF-8 text, this function can fail. In that case--- 'Nothing' is returned.--decodeText-  :: Bool           -- ^ Whether bit 11 of general-purpose bit flag is set-  -> ByteString     -- ^ Binary data to decode-  -> Maybe Text     -- ^ Decoded 'Text' in case of success-decodeText False = Just . decodeCP437-decodeText True  = either (const Nothing) Just . T.decodeUtf8'---- | Detect if the given text needs newer Unicode-aware features to be--- properly encoded in archive.--needsUnicode :: Text -> Bool-needsUnicode = not . T.all validCP437-  where validCP437 x = ord x <= 127---- | Convert numeric representation (as per .ZIP specification) of version--- into 'Version'.--toVersion :: Word16 -> Version-toVersion x = makeVersion [major, minor]-  where (major, minor) = quotRem (fromIntegral $ x .&. 0x00ff) 10---- | Covert 'Version' to its numeric representation as per .ZIP--- specification.--fromVersion :: Version -> Word16-fromVersion v = fromIntegral (major * 10 + minor)-  where (major:minor:_) = versionBranch v ++ repeat 0---- | Get compression method form its numeric representation.--toCompressionMethod :: Word16 -> Maybe CompressionMethod-toCompressionMethod 0  = Just Store-toCompressionMethod 8  = Just Deflate-toCompressionMethod 12 = Just BZip2-toCompressionMethod _  = Nothing---- | Convert 'CompressionMethod' to its numeric representation as per .ZIP--- specification.--fromCompressionMethod :: CompressionMethod -> Word16-fromCompressionMethod Store   = 0-fromCompressionMethod Deflate = 8-fromCompressionMethod BZip2   = 12---- | Check if an entry with these parameters needs Zip64 extension.--needsZip64 :: EntryDescription -> Bool-needsZip64 EntryDescription {..} = any (>= ffffffff)-  [edOffset, edCompressedSize, edUncompressedSize]---- | Determine “version needed to extract” that should be written to headers--- given need of Zip64 feature and compression method.--getZipVersion :: Bool -> Maybe CompressionMethod -> Version-getZipVersion zip64 m = max zip64ver mver-  where zip64ver = makeVersion (if zip64 then [4,5] else [2,0])-        mver     = makeVersion $ case m of-          Nothing      -> [2,0]-          Just Store   -> [2,0]-          Just Deflate -> [2,0]-          Just BZip2   -> [4,6]---- | Return decompressing 'Conduit' corresponding to the given compression--- method.--decompressingPipe-  :: MonadResource m-  => CompressionMethod-  -> Conduit ByteString m ByteString-decompressingPipe Store   = awaitForever yield-decompressingPipe Deflate = Z.decompress $ Z.WindowBits (-15)-decompressingPipe BZip2   = BZ.bunzip2---- | Sink that calculates CRC32 check sum for incoming stream.--crc32Sink :: Sink ByteString (ResourceT IO) Word32-crc32Sink = CL.fold crc32Update 0---- | Convert 'UTCTime' to MS-DOS time format.--toMsDosTime :: UTCTime -> MsDosTime-toMsDosTime UTCTime {..} = MsDosTime dosDate dosTime-  where-    dosTime = fromIntegral (seconds + shiftL minutes 5 + shiftL hours 11)-    dosDate = fromIntegral (day     + shiftL month   5 + shiftL year  9)--    seconds =-      let (MkFixed x) = todSec tod-      in fromIntegral (x `quot` 2000000000000)-    minutes = todMin tod-    hours   = todHour tod-    tod     = timeToTimeOfDay utctDayTime--    year    = fromIntegral year' - 1980-    (year', month, day) = toGregorian utctDay---- | Convert MS-DOS date-time to 'UTCTime'.--fromMsDosTime :: MsDosTime -> UTCTime-fromMsDosTime MsDosTime {..} = UTCTime-  (fromGregorian year month day)-  (secondsToDiffTime $ hours * 3600 + minutes * 60 + seconds)-  where-    seconds = fromIntegral $ 2 * (msDosTime     .&. 0x1f)-    minutes = fromIntegral (shiftR msDosTime 5  .&. 0x3f)-    hours   = fromIntegral (shiftR msDosTime 11 .&. 0x1f)--    day     = fromIntegral (msDosDate .&. 0x1f)-    month   = fromIntegral $ shiftR msDosDate 5 .&. 0x0f-    year    = 1980 + fromIntegral (shiftR msDosDate 9)---- | We use the constants of the type 'Natural' instead of literals to--- protect ourselves from overflows on 32 bit systems.--ffff, ffffffff :: Natural-ffff     = 0xffff-ffffffff = 0xffffffff+{-# LANGUAGE CPP #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TupleSections #-}++-- |+-- Module      :  Codec.Archive.Zip.Internal+-- Copyright   :  © 2016–present Mark Karpov+-- License     :  BSD 3 clause+--+-- Maintainer  :  Mark Karpov <markkarpov92@gmail.com>+-- Stability   :  experimental+-- Portability :  portable+--+-- Low-level, non-public types and operations.+module Codec.Archive.Zip.Internal where++import Codec.Archive.Zip.CP437 (decodeCP437)+import Codec.Archive.Zip.Internal.Type+import Conduit (PrimMonad)+import Control.Applicative (many, (<|>))+import Control.Exception (bracketOnError, catchJust)+import Control.Monad+import Control.Monad.Catch (MonadThrow (..))+import Control.Monad.Trans.Maybe+import Control.Monad.Trans.Resource (MonadResource, ResourceT)+import Data.Bits+import Data.Bool (bool)+import Data.ByteString (ByteString)+import Data.ByteString qualified as B+import Data.Char (ord)+import Data.Conduit (ConduitT, ZipSink (..), (.|))+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.Map.Strict (Map)+import Data.Map.Strict qualified as M+import Data.Maybe (catMaybes, fromJust, isNothing)+import Data.Sequence (Seq, (><), (|>))+import Data.Sequence qualified as S+import Data.Serialize+import Data.Set qualified as E+import Data.Text (Text)+import Data.Text qualified as T+import Data.Text.Encoding qualified as T+import Data.Time+import Data.Version+import Data.Void+import Data.Word (Word16, Word32)+import Numeric.Natural (Natural)+import System.Directory+import System.FilePath+import System.IO+import System.IO.Error (isDoesNotExistError)++#ifndef mingw32_HOST_OS+import qualified Codec.Archive.Zip.Unix as Unix+#endif++#ifdef ENABLE_BZIP2+import qualified Data.Conduit.BZlib as BZ+#endif++#ifdef ENABLE_ZSTD+import qualified Data.Conduit.Zstd as Zstandard+#endif++#if !MIN_VERSION_base(4,20,0)+import Data.Foldable (foldl')+#endif++----------------------------------------------------------------------------+-- Data types++-- | The sum type describes all possible actions that can be performed on an+-- archive.+data PendingAction+  = -- | Add an entry given its 'Source'+    SinkEntry+      CompressionMethod+      (ConduitT () ByteString (ResourceT IO) ())+      EntrySelector+  | -- | 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+  | -- | Delete an entry from archive+    DeleteEntry EntrySelector+  | -- | Change the compression method on an entry+    Recompress CompressionMethod EntrySelector+  | -- | Set the comment for a particular entry+    SetEntryComment Text EntrySelector+  | -- | Delete theh comment of a particular entry+    DeleteEntryComment EntrySelector+  | -- | Set the modification time of a particular entry+    SetModTime UTCTime EntrySelector+  | -- | Add an extra field to the specified entry+    AddExtraField Word16 ByteString EntrySelector+  | -- | Delete an extra filed of the specified entry+    DeleteExtraField Word16 EntrySelector+  | -- | Set the comment for the entire archive+    SetArchiveComment Text+  | -- | Delete the comment of the entire archive+    DeleteArchiveComment+  | -- | Set an external file attribute for the specified entry+    SetExternalFileAttributes Word32 EntrySelector++-- | A collection of maps describing how to produce entries in the resulting+-- archive.+data ProducingActions = ProducingActions+  { paCopyEntry :: Map FilePath (Map EntrySelector EntrySelector),+    paSinkEntry :: Map EntrySelector (EntryOrigin, ConduitT () ByteString (ResourceT IO) ())+  }++-- | A collection of editing actions, that is, actions that modify already+-- existing entries.+data EditingActions = EditingActions+  { eaCompression :: Map EntrySelector CompressionMethod,+    eaEntryComment :: Map EntrySelector Text,+    eaDeleteComment :: Map EntrySelector (),+    eaModTime :: Map EntrySelector UTCTime,+    eaExtraField :: Map EntrySelector (Map Word16 ByteString),+    eaDeleteField :: Map EntrySelector (Map Word16 ()),+    eaExtFileAttr :: Map EntrySelector Word32+  }++-- | 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 EntryOrigin+  | CentralDirHeader+  deriving (Eq)++-- | The data descriptor representation.+data DataDescriptor = DataDescriptor+  { ddCRC32 :: Word32,+    ddCompressedSize :: Natural,+    ddUncompressedSize :: Natural+  }++-- | A temporary data structure to hold Zip64 extra data field information.+data Zip64ExtraField = Zip64ExtraField+  { z64efUncompressedSize :: Natural,+    z64efCompressedSize :: Natural,+    z64efOffset :: Natural+  }++-- | MS-DOS date-time: a pair of 'Word16' (date, time) with the following+-- structure:+--+-- > DATE bit     0 - 4           5 - 8           9 - 15+-- >      value   day (1 - 31)    month (1 - 12)  years from 1980+-- > TIME bit     0 - 4           5 - 10          11 - 15+-- >      value   seconds*        minute          hour+-- >              *stored in two-second increments+data MsDosTime = MsDosTime+  { msDosDate :: Word16,+    msDosTime :: Word16+  }++----------------------------------------------------------------------------+-- Constants++-- | “Version created by” to specify when writing archive data.+zipVersion :: Version+zipVersion = Version [6, 3] []++----------------------------------------------------------------------------+-- Higher-level operations++-- | Scan the central directory of an archive and return its description+-- 'ArchiveDescription' as well as a collection of its entries.+--+-- This operation may fail with:+--+--     * @isAlreadyInUseError@ if the file is already open and cannot be+--     reopened;+--+--     * @isDoesNotExistError@ if the file does not exist;+--+--     * @isPermissionError@ if the user does not have permission to open+--     the file;+--+--     * 'ParsingFailed' when specified archive is something this library+--     cannot parse (this includes multi-disk archives, for example).+--+-- Please note that entries with invalid (non-portable) file names may be+-- missing in the list of entries. Files that are compressed with+-- unsupported compression methods are skipped as well. Also, if several+-- entries would collide on some operating systems (such as Windows, because+-- of its case-insensitivity), only one of them will be available, because+-- 'EntrySelector' is case-insensitive. These are the consequences of the+-- design decision to make it impossible to create non-portable archives+-- with this library.+scanArchive ::+  -- | Path to archive to scan+  FilePath ->+  IO (ArchiveDescription, Map EntrySelector EntryDescription)+scanArchive path = withBinaryFile path ReadMode $ \h -> do+  mecdOffset <- locateECD path h+  case mecdOffset of+    Just ecdOffset -> do+      hSeek h AbsoluteSeek ecdOffset+      ecdSize <- subtract ecdOffset <$> hFileSize h+      ecdRaw <- B.hGet h (fromIntegral ecdSize)+      case runGet getECD ecdRaw of+        Left msg -> throwM (ParsingFailed path msg)+        Right ecd -> do+          hSeek h AbsoluteSeek $ fromIntegral (adCDOffset ecd)+          cdRaw <- B.hGet h $ fromIntegral (adCDSize ecd)+          case runGet getCD cdRaw of+            Left msg -> throwM (ParsingFailed path msg)+            Right cd -> return (ecd, cd)+    Nothing ->+      throwM (ParsingFailed path "Cannot locate end of central directory")++-- | Given location of the archive and information about a specific archive+-- entry 'EntryDescription', return 'Source' of its data. The actual data+-- can be compressed or uncompressed depending on the third argument.+sourceEntry ::+  (PrimMonad m, MonadThrow m, MonadResource m) =>+  -- | Path to archive that contains the entry+  FilePath ->+  -- | Information needed to extract entry of interest+  EntryDescription ->+  -- | Should we stream uncompressed data?+  Bool ->+  -- | Source of uncompressed data+  ConduitT () ByteString m ()+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+    decompress =+      if d+        then decompressingPipe edCompression+        else C.awaitForever C.yield++-- | Undertake /all/ actions specified as the fourth argument of the+-- function. This transforms the given pending actions so they can be+-- performed in one pass, and then they are applied in the most efficient+-- way.+commit ::+  -- | Location of archive file to edit or create+  FilePath ->+  -- | Archive description+  ArchiveDescription ->+  -- | 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 copying sinking, editing) =+          optimize (toRecreatingActions path entries >< xs)+        comment = predictComment adComment xs+    copiedCD <-+      M.unions+        <$> M.traverseWithKey+          ( \srcPath m ->+              copyEntries h srcPath m editing+          )+          copying+    sunkCD <-+      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+-- the old file will be intact. The file is only updated\/replaced if the+-- second argument finishes without exceptions.+withNewFile ::+  -- | Name of file to create+  FilePath ->+  -- | Action that writes to given 'Handle'+  (Handle -> IO ()) ->+  IO ()+withNewFile fpath action =+  bracketOnError allocate release $ \(path, h) -> do+    action h+    hClose h+    renameFile path fpath+  where+    allocate = openBinaryTempFile (takeDirectory fpath) ".zip"+    release (path, h) = do+      hClose h+      -- Despite using `bracketOnError` the file is not guaranteed to exist+      -- here since we could be interrupted with an async exception after+      -- the file has been renamed. Therefore, we silentely ignore+      -- `DoesNotExistError`.+      catchJust (guard . isDoesNotExistError) (removeFile path) (const $ pure ())++-- | Determine what comment in new archive will look like given its original+-- value and a collection of pending actions.+predictComment :: Maybe Text -> Seq PendingAction -> Maybe Text+predictComment original xs =+  case S.index xs <$> S.findIndexR (isNothing . targetEntry) xs of+    Nothing -> original+    Just DeleteArchiveComment -> Nothing+    Just (SetArchiveComment txt) -> Just txt+    Just _ -> Nothing++-- | Transform a map representing existing entries into a collection of+-- actions that re-create those entires.+toRecreatingActions ::+  -- | Name of the archive file where entires are found+  FilePath ->+  -- | Actual list of entires+  Map EntrySelector EntryDescription ->+  -- | Actions that recreate the archive entries+  Seq PendingAction+toRecreatingActions path entries = E.foldl' f S.empty (M.keysSet entries)+  where+    f s e = s |> CopyEntry path e e++-- | Transform a collection of 'PendingAction's into 'ProducingActions' and+-- 'EditingActions'—data that describes how to create resulting archive.+optimize ::+  -- | Collection of pending actions+  Seq PendingAction ->+  -- | Optimized data+  (ProducingActions, EditingActions)+optimize =+  foldl'+    f+    ( ProducingActions M.empty M.empty,+      EditingActions M.empty M.empty M.empty M.empty M.empty M.empty M.empty+    )+  where+    f (pa, ea) a = case a of+      SinkEntry m src s ->+        ( 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),+              paCopyEntry = M.alter (ef os ns) path (paCopyEntry pa)+            },+          clearEditingFor ns ea+        )+      RenameEntry os ns ->+        ( pa+            { paCopyEntry = M.map (M.map $ re os ns) (paCopyEntry pa),+              paSinkEntry = renameKey os ns (paSinkEntry pa)+            },+          ea+            { eaCompression = renameKey os ns (eaCompression ea),+              eaEntryComment = renameKey os ns (eaEntryComment ea),+              eaDeleteComment = renameKey os ns (eaDeleteComment ea),+              eaModTime = renameKey os ns (eaModTime ea),+              eaExtraField = renameKey os ns (eaExtraField ea),+              eaDeleteField = renameKey os ns (eaDeleteField ea)+            }+        )+      DeleteEntry s ->+        ( pa+            { paSinkEntry = M.delete s (paSinkEntry pa),+              paCopyEntry = M.map (M.delete s) (paCopyEntry pa)+            },+          clearEditingFor s ea+        )+      Recompress m s ->+        (pa, ea {eaCompression = M.insert s m (eaCompression ea)})+      SetEntryComment txt s ->+        ( pa,+          ea+            { eaEntryComment = M.insert s txt (eaEntryComment ea),+              eaDeleteComment = M.delete s (eaDeleteComment ea)+            }+        )+      DeleteEntryComment s ->+        ( pa,+          ea+            { eaEntryComment = M.delete s (eaEntryComment ea),+              eaDeleteComment = M.insert s () (eaDeleteComment ea)+            }+        )+      SetModTime time s ->+        (pa, ea {eaModTime = M.insert s time (eaModTime ea)})+      AddExtraField n b s ->+        ( pa,+          ea+            { eaExtraField = M.alter (ef n b) s (eaExtraField ea),+              eaDeleteField = M.delete s (eaDeleteField ea)+            }+        )+      DeleteExtraField n s ->+        ( pa,+          ea+            { eaExtraField = M.alter (er n) s (eaExtraField ea),+              eaDeleteField = M.alter (ef n ()) s (eaDeleteField ea)+            }+        )+      SetExternalFileAttributes b s ->+        ( pa,+          ea {eaExtFileAttr = M.insert s b (eaExtFileAttr ea)}+        )+      _ -> (pa, ea)+    clearEditingFor s ea =+      ea+        { eaCompression = M.delete s (eaCompression ea),+          eaEntryComment = M.delete s (eaEntryComment ea),+          eaDeleteComment = M.delete s (eaDeleteComment ea),+          eaModTime = M.delete s (eaModTime ea),+          eaExtraField = M.delete s (eaExtraField ea),+          eaDeleteField = M.delete s (eaDeleteField ea),+          eaExtFileAttr = M.delete s (eaExtFileAttr ea)+        }+    re o n x = if x == o then n else x+    ef k v (Just m) = Just (M.insert k v m)+    ef k v Nothing = Just (M.singleton k v)+    er k (Just m) =+      let n = M.delete k m+       in if M.null n then Nothing else Just n+    er _ Nothing = Nothing++-- | Copy entries from another archive and write them into the file+-- associated with the given handle. This can throw 'EntryDoesNotExist' if+-- there is no such entry in that archive.+copyEntries ::+  -- | Opened 'Handle' of zip archive file+  Handle ->+  -- | Path to the file to copy the entries from+  FilePath ->+  -- | 'Map' from original name to name to use in new archive+  Map EntrySelector EntrySelector ->+  -- | Additional info that can influence result+  EditingActions ->+  -- | Info to generate central directory file headers later+  IO (Map EntrySelector EntryDescription)+copyEntries h path names editing = do+  entries <- snd <$> scanArchive path+  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'.+sinkEntry ::+  -- | Opened 'Handle' of zip archive file+  Handle ->+  -- | Name of the entry to add+  EntrySelector ->+  -- | Origin of the entry (can contain additional info)+  EntryOrigin ->+  -- | Source of the entry contents+  ConduitT () ByteString (ResourceT IO) () ->+  -- | Additional info that can influence result+  EditingActions ->+  -- | Info to generate the central directory file headers later+  IO EntryDescription+sinkEntry h s o src EditingActions {..} = do+  currentTime <- getCurrentTime+  offset <- hTell h+  let compressed = case o of+        Borrowed ed -> edCompression ed+        _ -> Store+      compression = M.findWithDefault compressed s eaCompression+      recompression = compression /= compressed+      modTime = case o of+        Borrowed ed -> edModTime ed+        _ -> currentTime+      extFileAttr = M.findWithDefault defaultFileMode s eaExtFileAttr+      oldExtraFields = case o of+        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+        (Borrowed ed, Nothing) -> edComment ed+        _ -> Nothing+      desc0 =+        EntryDescription -- to write in local header+          { edVersionMadeBy = zipVersion,+            edVersionNeeded = zipVersion,+            edCompression = compression,+            edModTime = M.findWithDefault modTime s eaModTime,+            edCRC32 = 0, -- to be overwritten after streaming+            edCompressedSize = 0, -- ↑+            edUncompressedSize = 0, -- ↑+            edOffset = fromIntegral offset,+            edComment = M.lookup s eaEntryComment <|> oldComment,+            edExtraField = extraField,+            edExternalFileAttrs = extFileAttr+          }+  B.hPut h (runPut (putHeader (LocalHeader o) s desc0))+  DataDescriptor {..} <-+    C.runConduitRes $+      if recompression+        then+          if compressed == Store+            then src .| sinkData h compression+            else src .| decompressingPipe compressed .| sinkData h compression+        else src .| sinkData h Store+  afterStreaming <- hTell h+  let desc1 = case o of+        Borrowed ed ->+          desc0+            { edCRC32 =+                bool (edCRC32 ed) ddCRC32 recompression,+              edCompressedSize =+                bool (edCompressedSize ed) ddCompressedSize recompression,+              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 o) s desc2))+  hSeek h AbsoluteSeek afterStreaming+  return desc2++{- ORMOLU_DISABLE -}++-- | Create a 'Sink' to stream data there. Once streaming is finished,+-- return 'DataDescriptor' for the streamed data. The action /does not/+-- close the given 'Handle'.+sinkData ::+  -- | Opened 'Handle' of zip archive file+  Handle ->+  -- | Compression method to apply+  CompressionMethod ->+  -- | 'Sink' where to stream data+  ConduitT ByteString Void (ResourceT IO) DataDescriptor+sinkData h compression = do+  let sizeSink = CL.fold (\acc input -> fromIntegral (B.length input) + acc) 0+      dataSink =+        getZipSink $+          ZipSink sizeSink <* ZipSink (CB.sinkHandle h)+      withCompression sink =+        getZipSink $+          (,,) <$> ZipSink sizeSink+            <*> ZipSink crc32Sink+            <*> ZipSink sink+  (uncompressedSize, crc32, compressedSize) <-+    case compression of+      Store ->+        withCompression+          dataSink+      Deflate ->+        withCompression $+          Z.compress 9 (Z.WindowBits (-15)) .| dataSink+#ifdef ENABLE_BZIP2+      BZip2 ->+        withCompression $+          BZ.bzip2 .| dataSink+#else+      BZip2 -> throwM (UnsupportedCompressionMethod BZip2)+#endif+#ifdef ENABLE_ZSTD+      Zstd ->+        withCompression $+          Zstandard.compress 1 .| dataSink+#else+      Zstd -> throwM (UnsupportedCompressionMethod Zstd)+#endif+  return+    DataDescriptor+      { ddCRC32 = fromIntegral crc32,+        ddCompressedSize = compressedSize,+        ddUncompressedSize = uncompressedSize+      }++{- ORMOLU_ENABLE -}++-- | Append central directory entries and the end of central directory+-- record to the file that given 'Handle' is associated with. Note that this+-- automatically writes Zip64 end of central directory record and Zip64 end+-- of central directory locator when necessary.+writeCD ::+  -- | Opened handle of zip archive file+  Handle ->+  -- | Commentary to the entire archive+  Maybe Text ->+  -- | Info about already written local headers and entry data+  Map EntrySelector EntryDescription ->+  IO ()+writeCD h comment m = do+  let cd = runPut (putCD m)+  cdOffset <- fromIntegral <$> hTell h+  B.hPut h cd -- write central directory+  let totalCount = fromIntegral (M.size m)+      cdSize = fromIntegral (B.length cd)+      needZip64 =+        totalCount >= ffff+          || cdSize >= ffffffff+          || cdOffset >= ffffffff+  when needZip64 $ do+    zip64ecdOffset <- fromIntegral <$> hTell h+    (B.hPut h . runPut) (putZip64ECD totalCount cdSize cdOffset)+    (B.hPut h . runPut) (putZip64ECDLocator zip64ecdOffset)+  (B.hPut h . runPut) (putECD totalCount cdSize cdOffset comment)++----------------------------------------------------------------------------+-- Binary serialization++-- | Extract the number of bytes between the start of file name in local+-- header and the start of actual data.+getLocalHeaderGap :: Get Integer+getLocalHeaderGap = do+  getSignature 0x04034b50+  skip 2 -- version needed to extract+  skip 2 -- general purpose bit flag+  skip 2 -- compression method+  skip 2 -- last mod file time+  skip 2 -- last mod file date+  skip 4 -- crc-32 check sum+  skip 4 -- compressed size+  skip 4 -- uncompressed size+  fileNameSize <- fromIntegral <$> getWord16le -- file name length+  extraFieldSize <- fromIntegral <$> getWord16le -- extra field length+  return (fileNameSize + extraFieldSize)++-- | Parse central directory file headers and put them into a 'Map'.+getCD :: Get (Map EntrySelector EntryDescription)+getCD = M.fromList . catMaybes <$> many getCDHeader++-- | Parse a single central directory file header. If it's a directory or+-- file compressed with unsupported compression method, 'Nothing' is+-- returned.+getCDHeader :: Get (Maybe (EntrySelector, EntryDescription))+getCDHeader = do+  getSignature 0x02014b50 -- central file header signature+  versionMadeBy <- toVersion <$> getWord16le -- version made by+  versionNeeded <- toVersion <$> getWord16le -- version needed to extract+  when (versionNeeded > zipVersion) . fail $+    "Version required to extract the archive is "+      ++ showVersion versionNeeded+      ++ " (can do "+      ++ showVersion zipVersion+      ++ ")"+  bitFlag <- getWord16le -- general purpose bit flag+  when (any (testBit bitFlag) [0, 6, 13]) . fail $+    "Encrypted archives are not supported"+  let needUnicode = testBit bitFlag 11+  mcompression <- toCompressionMethod <$> getWord16le -- compression method+  modTime <- getWord16le -- last mod file time+  modDate <- getWord16le -- last mod file date+  crc32 <- getWord32le -- CRC32 check sum+  compressed <- fromIntegral <$> getWord32le -- compressed size+  uncompressed <- fromIntegral <$> getWord32le -- uncompressed size+  fileNameSize <- getWord16le -- file name length+  extraFieldSize <- getWord16le -- extra field length+  commentSize <- getWord16le -- file comment size+  skip 4 -- disk number start, internal file attributes+  externalFileAttrs <- getWord32le -- external file attributes+  offset <- fromIntegral <$> getWord32le -- offset of local header+  fileName <-+    decodeText needUnicode+      <$> getBytes (fromIntegral fileNameSize) -- file name+  extraField <-+    M.fromList+      <$> isolate (fromIntegral extraFieldSize) (many getExtraField)+  -- ↑ extra fields in their raw form+  comment <- decodeText needUnicode <$> getBytes (fromIntegral commentSize)+  -- ↑ file comment+  let dfltZip64 =+        Zip64ExtraField+          { z64efUncompressedSize = uncompressed,+            z64efCompressedSize = compressed,+            z64efOffset = offset+          }+      z64ef = case M.lookup 1 extraField of+        Nothing -> dfltZip64+        Just b -> parseZip64ExtraField dfltZip64 b+  case mcompression of+    Nothing -> return Nothing+    Just compression ->+      let desc =+            EntryDescription+              { edVersionMadeBy = versionMadeBy,+                edVersionNeeded = versionNeeded,+                edCompression = compression,+                edModTime = fromMsDosTime (MsDosTime modDate modTime),+                edCRC32 = crc32,+                edCompressedSize = z64efCompressedSize z64ef,+                edUncompressedSize = z64efUncompressedSize z64ef,+                edOffset = z64efOffset z64ef,+                edComment = if commentSize == 0 then Nothing else comment,+                edExtraField = extraField,+                edExternalFileAttrs = externalFileAttrs+              }+       in return $ (,desc) <$> (fileName >>= mkEntrySelector . T.unpack)++-- | Parse an extra-field.+getExtraField :: Get (Word16, ByteString)+getExtraField = do+  header <- getWord16le -- header id+  size <- getWord16le -- data size+  body <- getBytes (fromIntegral size) -- content+  return (header, body)++-- | Get signature. If the extracted data is not equal to the provided+-- signature, fail.+getSignature :: Word32 -> Get ()+getSignature sig = do+  x <- getWord32le -- grab 4-byte signature+  unless (x == sig) . fail $+    "Expected signature " ++ show sig ++ ", but got: " ++ show x++-- | Parse 'Zip64ExtraField' from its binary representation.+parseZip64ExtraField ::+  -- | What is read from central directory file header+  Zip64ExtraField ->+  -- | Actual binary representation+  ByteString ->+  -- | Result+  Zip64ExtraField+parseZip64ExtraField dflt@Zip64ExtraField {..} b =+  either (const dflt) id . flip runGet b $ do+    let ifsat v =+          if v >= ffffffff+            then fromIntegral <$> getWord64le+            else return v+    uncompressed <- ifsat z64efUncompressedSize -- uncompressed size+    compressed <- ifsat z64efCompressedSize -- compressed size+    offset <- ifsat z64efOffset -- offset of local file header+    return (Zip64ExtraField uncompressed compressed offset)++-- | Produce binary representation of 'Zip64ExtraField'.+makeZip64ExtraField ::+  -- | Is this for local or central directory header?+  HeaderType ->+  -- | Zip64 extra field's data+  Zip64ExtraField ->+  -- | Resulting representation+  ByteString+makeZip64ExtraField headerType Zip64ExtraField {..} = runPut $ do+  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.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.foldMapWithKey (putHeader CentralDirHeader)++-- | Create 'ByteString' representing either a local file header or a+-- central directory file header.+putHeader ::+  -- | Type of header to generate+  HeaderType ->+  -- | Name of entry to write+  EntrySelector ->+  -- | Description of entry+  EntryDescription ->+  Put+putHeader headerType s entry@EntryDescription {..} = do+  let isCentralDirHeader = headerType == CentralDirHeader+  putWord32le (bool 0x04034b50 0x02014b50 isCentralDirHeader)+  -- ↑ local/central file header signature+  when isCentralDirHeader $+    putWord16le (fromVersion edVersionMadeBy) -- version made by+  putWord16le (fromVersion edVersionNeeded) -- version needed to extract+  let entryName = getEntryName s+      rawName = T.encodeUtf8 entryName+      comment = B.take 0xffff (maybe B.empty T.encodeUtf8 edComment)+      unicode =+        needsUnicode entryName+          || maybe False needsUnicode edComment+      modTime = toMsDosTime edModTime+  putWord16le (if unicode then setBit 0 11 else 0)+  -- ↑ general purpose bit-flag+  putWord16le (fromCompressionMethod edCompression) -- compression method+  putWord16le (msDosTime modTime) -- last mod file time+  putWord16le (msDosDate modTime) -- last mod file date+  putWord32le edCRC32 -- CRC-32 checksum+  putWord32le (withSaturation edCompressedSize) -- compressed size+  putWord32le (withSaturation edUncompressedSize) -- uncompressed size+  putWord16le (fromIntegral $ B.length rawName) -- file name length+  let zip64ef =+        makeZip64ExtraField+          headerType+          Zip64ExtraField+            { z64efUncompressedSize = edUncompressedSize,+              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 appendZip64+            then M.insert 1 zip64ef edExtraField+            else edExtraField+  putWord16le (fromIntegral $ B.length extraField) -- extra field length+  when isCentralDirHeader $ do+    putWord16le (fromIntegral $ B.length comment) -- file comment length+    putWord16le 0 -- disk number start+    putWord16le 0 -- internal file attributes+    putWord32le edExternalFileAttrs -- external file attributes+    putWord32le (withSaturation edOffset) -- relative offset of local header+  putByteString rawName -- file name (variable size)+  putByteString extraField -- extra field (variable size)+  when isCentralDirHeader (putByteString comment) -- file comment (variable size)++-- | Create 'ByteString' representing Zip64 end of central directory record.+putZip64ECD ::+  -- | Total number of entries+  Natural ->+  -- | Size of the central directory+  Natural ->+  -- | Offset of central directory record+  Natural ->+  Put+putZip64ECD totalCount cdSize cdOffset = do+  putWord32le 0x06064b50 -- zip64 end of central dir signature+  putWord64le 44 -- size of zip64 end of central dir record+  putWord16le (fromVersion zipVersion) -- version made by+  putWord16le (fromVersion $ getZipVersion True Nothing)+  -- ↑ version needed to extract+  putWord32le 0 -- number of this disk+  putWord32le 0 -- number of the disk with the start of the central directory+  putWord64le (fromIntegral totalCount) -- total number of entries (this disk)+  putWord64le (fromIntegral totalCount) -- total number of entries+  putWord64le (fromIntegral cdSize) -- size of the central directory+  putWord64le (fromIntegral cdOffset) -- offset of central directory++-- | Create 'ByteString' representing Zip64 end of the central directory+-- locator.+putZip64ECDLocator ::+  -- | Offset of Zip64 end of central directory+  Natural ->+  Put+putZip64ECDLocator ecdOffset = do+  putWord32le 0x07064b50 -- zip64 end of central dir locator signature+  putWord32le 0 -- number of the disk with the start of the zip64 end of+  -- central directory+  putWord64le (fromIntegral ecdOffset) -- relative offset of the zip64 end+  -- of central directory record+  putWord32le 1 -- total number of disks++-- | Parse end of the central directory record or Zip64 end of the central+-- directory record depending on signature binary data begins with.+getECD :: Get ArchiveDescription+getECD = do+  sig <- getWord32le -- end of central directory signature+  let zip64 = sig == 0x06064b50+  unless (sig == 0x06054b50 || sig == 0x06064b50) $+    fail "Cannot locate end of central directory"+  zip64size <-+    if zip64+      then do+        x <- getWord64le -- size of zip64 end of central directory record+        skip 2 -- version made by+        skip 2 -- version needed to extract+        return (Just x)+      else return Nothing+  thisDisk <- bool (fromIntegral <$> getWord16le) getWord32le zip64+  -- ↑ number of this disk+  cdDisk <- bool (fromIntegral <$> getWord16le) getWord32le zip64+  -- ↑ number of the disk with the start of the central directory+  unless (thisDisk == 0 && cdDisk == 0) $+    fail "No support for multi-disk archives"+  skip (bool 2 8 zip64)+  -- ↑ total number of entries in the central directory on this disk+  skip (bool 2 8 zip64)+  -- ↑ total number of entries in the central directory+  cdSize <- bool (fromIntegral <$> getWord32le) getWord64le zip64+  -- ↑ size of the central directory+  cdOffset <- bool (fromIntegral <$> getWord32le) getWord64le zip64+  -- ↑ offset of start of central directory with respect to the starting+  -- disk number+  when zip64 . skip . fromIntegral $ fromJust zip64size - 4 -- obviously+  commentSize <- getWord16le -- .ZIP file comment length+  comment <- decodeText True <$> getBytes (fromIntegral commentSize)+  -- ↑ archive comment, it's uncertain how we should decide on encoding here+  return+    ArchiveDescription+      { adComment = if commentSize == 0 then Nothing else comment,+        adCDOffset = fromIntegral cdOffset,+        adCDSize = fromIntegral cdSize+      }++-- | Create a 'ByteString' representing the end of central directory record.+putECD ::+  -- | Total number of entries+  Natural ->+  -- | Size of the central directory+  Natural ->+  -- | Offset of central directory record+  Natural ->+  -- | Zip file comment+  Maybe Text ->+  Put+putECD totalCount cdSize cdOffset mcomment = do+  putWord32le 0x06054b50 -- end of central dir signature+  putWord16le 0 -- number of this disk+  putWord16le 0 -- number of the disk with the start of the central directory+  putWord16le (withSaturation totalCount)+  -- ↑ total number of entries on this disk+  putWord16le (withSaturation totalCount) -- total number of entries+  putWord32le (withSaturation cdSize) -- size of central directory+  putWord32le (withSaturation cdOffset) -- offset of start of central directory+  let comment = maybe B.empty T.encodeUtf8 mcomment+  putWord16le (fromIntegral $ B.length comment)+  putByteString comment++-- | Find the absolute offset of the end of central directory record or, if+-- present, Zip64 end of central directory record.+locateECD :: FilePath -> Handle -> IO (Maybe Integer)+locateECD path h = sizeCheck+  where+    sizeCheck = do+      fsize <- hFileSize h+      let limit = max 0 (fsize - 0xffff - 22)+      if fsize < 22+        then return Nothing+        else hSeek h SeekFromEnd (-22) >> loop limit+    loop limit = do+      sig <- getNum getWord32le 4+      pos <- subtract 4 <$> hTell h+      let again = hSeek h AbsoluteSeek (pos - 1) >> loop limit+          done = pos <= limit+      if sig == 0x06054b50+        then do+          result <-+            runMaybeT $+              MaybeT (checkComment pos)+                >>= MaybeT . checkCDSig+                >>= MaybeT . checkZip64+          case result of+            Nothing -> bool again (return Nothing) done+            Just ecd -> return (Just ecd)+        else bool again (return Nothing) done+    checkComment pos = do+      size <- hFileSize h+      hSeek h AbsoluteSeek (pos + 20)+      l <- fromIntegral <$> getNum getWord16le 2+      return $+        if l + 22 == size - pos+          then Just pos+          else Nothing+    checkCDSig pos = do+      hSeek h AbsoluteSeek (pos + 16)+      sigPos <- fromIntegral <$> getNum getWord32le 4+      if sigPos == 0xffffffff -- Zip64 is probably used+        then return (Just pos)+        else do+          hSeek h AbsoluteSeek sigPos+          cdSig <- getNum getWord32le 4+          return $+            if cdSig == 0x02014b50+              ||+              -- ↑ normal case: central directory file header signature+              cdSig == 0x06064b50+              ||+              -- ↑ happens when zip 64 archive is empty+              cdSig == 0x06054b50+              then -- ↑ happens when vanilla archive is empty+                Just pos+              else Nothing+    checkZip64 pos =+      if pos < 20+        then return (Just pos)+        else do+          hSeek h AbsoluteSeek (pos - 20)+          zip64locatorSig <- getNum getWord32le 4+          if zip64locatorSig == 0x07064b50+            then do+              hSeek h AbsoluteSeek (pos - 12)+              Just . fromIntegral <$> getNum getWord64le 8+            else return (Just pos)+    getNum f n = do+      result <- runGet f <$> B.hGet h n+      case result of+        Left msg -> throwM (ParsingFailed path msg)+        Right val -> return val++----------------------------------------------------------------------------+-- Helpers++-- | Rename an entry (key) in a 'Map'.+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)++-- | Like 'fromIntegral', but with saturation when converting to bounded+-- types.+withSaturation :: forall a b. (Integral a, Integral b, Bounded b) => a -> b+withSaturation x =+  if (fromIntegral x :: Integer) > (fromIntegral bound :: Integer)+    then bound+    else fromIntegral x+  where+    bound = maxBound :: b++-- | 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+targetEntry (Recompress _ s) = Just s+targetEntry (SetEntryComment _ s) = Just s+targetEntry (DeleteEntryComment s) = Just s+targetEntry (SetModTime _ s) = Just s+targetEntry (AddExtraField _ _ s) = Just s+targetEntry (DeleteExtraField _ s) = Just s+targetEntry (SetExternalFileAttributes _ s) = Just s+targetEntry (SetArchiveComment _) = Nothing+targetEntry DeleteArchiveComment = Nothing++-- | Decode a 'ByteString'. The first argument indicates whether we should+-- treat it as UTF-8 (in case bit 11 of general-purpose bit flag is set),+-- otherwise the function assumes CP437. Note that since not every stream of+-- bytes constitutes valid UTF-8 text, this function can fail. In that case+-- 'Nothing' is returned.+decodeText ::+  -- | Whether bit 11 of general-purpose bit flag is set+  Bool ->+  -- | Binary data to decode+  ByteString ->+  -- | Decoded 'Text' in case of success+  Maybe Text+decodeText False = Just . decodeCP437+decodeText True = either (const Nothing) Just . T.decodeUtf8'++-- | Detect if the given text needs newer Unicode-aware features to be+-- properly encoded in the archive.+needsUnicode :: Text -> Bool+needsUnicode = not . T.all validCP437+  where+    validCP437 x = ord x <= 127++-- | Convert numeric representation (as per the .ZIP specification) of+-- version into 'Version'.+toVersion :: Word16 -> Version+toVersion x = makeVersion [major, minor]+  where+    (major, minor) = quotRem (fromIntegral $ x .&. 0x00ff) 10++-- | Covert 'Version' to its numeric representation as per the .ZIP+-- specification.+fromVersion :: Version -> Word16+fromVersion v = fromIntegral ((ZIP_OS `shiftL` 8) .|. (major * 10 + minor))+  where+    (major, minor) =+      case versionBranch v of+        v0 : v1 : _ -> (v0, v1)+        v0 : _ -> (v0, 0)+        [] -> (0, 0)++-- | Get the compression method form its numeric representation.+toCompressionMethod :: Word16 -> Maybe CompressionMethod+toCompressionMethod 0 = Just Store+toCompressionMethod 8 = Just Deflate+toCompressionMethod 12 = Just BZip2+toCompressionMethod 93 = Just Zstd+toCompressionMethod _ = Nothing++-- | Convert 'CompressionMethod' to its numeric representation as per the+-- .ZIP specification.+fromCompressionMethod :: CompressionMethod -> Word16+fromCompressionMethod Store = 0+fromCompressionMethod Deflate = 8+fromCompressionMethod BZip2 = 12+fromCompressionMethod Zstd = 93++-- | Check if an entry with these parameters needs the Zip64 extension.+needsZip64 :: EntryDescription -> Bool+needsZip64 EntryDescription {..} =+  any+    (>= 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+getZipVersion zip64 m = max zip64ver mver+  where+    zip64ver = makeVersion (if zip64 then [4, 5] else [2, 0])+    mver = makeVersion $ case m of+      Nothing -> [2, 0]+      Just Store -> [2, 0]+      Just Deflate -> [2, 0]+      Just BZip2 -> [4, 6]+      Just Zstd -> [6, 3]++-- | Return a decompressing 'Conduit' corresponding to the given compression+-- method.+decompressingPipe ::+  (PrimMonad m, MonadThrow m, MonadResource m) =>+  CompressionMethod ->+  ConduitT ByteString ByteString m ()+decompressingPipe Store = C.awaitForever C.yield+decompressingPipe Deflate = Z.decompress $ Z.WindowBits (-15)++#ifdef ENABLE_BZIP2+decompressingPipe BZip2 = BZ.bunzip2+#else+decompressingPipe BZip2 = throwM (UnsupportedCompressionMethod BZip2)+#endif++#ifdef ENABLE_ZSTD+decompressingPipe Zstd = Zstandard.decompress+#else+decompressingPipe Zstd = throwM (UnsupportedCompressionMethod Zstd)+#endif++-- | A sink that calculates the CRC32 check sum for an incoming stream.+crc32Sink :: ConduitT ByteString Void (ResourceT IO) Word32+crc32Sink = CL.fold crc32Update 0++-- | Convert 'UTCTime' to the MS-DOS time format.+toMsDosTime :: UTCTime -> MsDosTime+toMsDosTime UTCTime {..} = MsDosTime dosDate dosTime+  where+    dosTime = fromIntegral (seconds + shiftL minutes 5 + shiftL hours 11)+    dosDate = fromIntegral (day + shiftL month 5 + shiftL year 9)+    seconds =+      let (MkFixed x) = todSec tod+       in fromIntegral (x `quot` 2000000000000)+    minutes = todMin tod+    hours = todHour tod+    tod = timeToTimeOfDay utctDayTime+    year = fromIntegral year' - 1980+    (year', month, day) = toGregorian utctDay++-- | Convert MS-DOS date-time to 'UTCTime'.+fromMsDosTime :: MsDosTime -> UTCTime+fromMsDosTime MsDosTime {..} =+  UTCTime+    (fromGregorian year month day)+    (secondsToDiffTime $ hours * 3600 + minutes * 60 + seconds)+  where+    seconds = fromIntegral $ 2 * (msDosTime .&. 0x1f)+    minutes = fromIntegral (shiftR msDosTime 5 .&. 0x3f)+    hours = fromIntegral (shiftR msDosTime 11 .&. 0x1f)+    day = fromIntegral (msDosDate .&. 0x1f)+    month = fromIntegral $ shiftR msDosDate 5 .&. 0x0f+    year = 1980 + fromIntegral (shiftR msDosDate 9)++-- We use the constants of the type 'Natural' instead of literals to protect+-- ourselves from overflows on 32 bit systems.+--+-- If we're in the development mode, use lower values so the tests get a+-- chance to check all cases (otherwise we would need to generate way too+-- big archives on CI).++ffff, ffffffff :: Natural++#ifdef HASKELL_ZIP_DEV_MODE+ffff     = 25+ffffffff = 250+#else+ffff     = 0xffff+ffffffff = 0xffffffff+#endif++-- | The default permissions for the files, permissions not set on Windows,+-- and are set to rw on Unix. This mimics the behavior of the zip utility.+defaultFileMode :: Word32++#ifdef mingw32_HOST_OS+defaultFileMode = 0++#else+defaultFileMode = Unix.fromFileMode 0o600+#endif
+ 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,207 +0,0 @@--- |--- Module      :  Codec.Archive.Zip.Type--- Copyright   :  © 2016–2017 Mark Karpov--- License     :  BSD 3 clause------ Maintainer  :  Mark Karpov <markkarpov92@gmail.com>--- Stability   :  experimental--- Portability :  portable------ Types used by the package. You don't usually need to import this module,--- because "Codec.Archive.Zip" re-exports everything you may need, import--- that module instead.--{-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE DeriveGeneric      #-}--module Codec.Archive.Zip.Type-  ( -- * Entry selector-    EntrySelector-  , mkEntrySelector-  , unEntrySelector-  , getEntryName-  , EntrySelectorException (..)-    -- * Entry description-  , EntryDescription (..)-  , CompressionMethod (..)-    -- * Archive desrciption-  , ArchiveDescription (..)-    -- * Exceptions-  , ZipException (..) )-where--import Control.Arrow ((>>>))-import Control.Exception (Exception)-import Control.Monad.Catch (MonadThrow (..))-import Data.ByteString (ByteString)-import Data.CaseInsensitive (CI)-import Data.Data (Data)-import Data.List.NonEmpty (NonEmpty)-import Data.Map (Map)-import Data.Maybe (mapMaybe, fromJust)-import Data.Text (Text)-import Data.Time.Clock (UTCTime)-import Data.Typeable (Typeable)-import Data.Version (Version)-import Data.Word (Word16, Word32)-import GHC.Generics (Generic)-import Numeric.Natural-import Path-import qualified Data.ByteString         as B-import qualified Data.CaseInsensitive    as CI-import qualified Data.List.NonEmpty      as NE-import qualified Data.Text               as T-import qualified Data.Text.Encoding      as T-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 select--- files in archive or to name new files.------ The abstraction is crucial for ensuring that created archives are--- portable across operating systems, file systems, and different 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 archive for compatibility with Unix-like operating--- systems (as it is recommended in the specification). On the other hand,--- in can be rendered as ordinary relative file path in OS-specific format,--- when needed.--newtype EntrySelector = EntrySelector-  { unES :: NonEmpty (CI String)-    -- ^ Path pieces of relative path inside archive-  } deriving (Eq, Ord, Typeable, Data, Generic)--instance Show EntrySelector where-  show = show . unEntrySelector---- | Create an 'EntrySelector' from @'Path' 'Rel' 'File'@. To avoid problems--- with distribution of the archive, characters that some operating systems--- do not expect in paths are not allowed. Proper paths should pass these--- checks:------     * 'System.FilePath.Posix.isValid'---     * 'System.FilePath.Windows.isValid'---     * binary representation of normalized path should be not longer than---       65535 bytes------ This function can throw an 'EntrySelectorException'.--mkEntrySelector :: MonadThrow m => Path Rel File -> m EntrySelector-mkEntrySelector path =-  let fp           = toFilePath path-      g x          = if null x then Nothing else Just (CI.mk x)-      preparePiece = g . filter (not . FP.isPathSeparator)-      pieces       = mapMaybe preparePiece (FP.splitPath fp)-      selector     = EntrySelector (NE.fromList pieces)-      binLength    = B.length . T.encodeUtf8 . getEntryName-  in if Posix.isValid fp   &&-        Windows.isValid fp &&-        fp /= "."          && -- work around a bug in the path package-        not (null pieces)  &&-        binLength selector <= 0xffff-       then return selector-       else throwM (InvalidEntrySelector path)---- | Make a relative path from 'EntrySelector'. Every 'EntrySelector'--- produces a single @'Path' 'Rel' 'File'@ that corresponds to it.--unEntrySelector :: EntrySelector -> Path Rel File-unEntrySelector = unES-  >>> NE.toList-  >>> fmap CI.original-  >>> FP.joinPath-  >>> parseRelFile-  >>> fromJust---- | Get an entry name in the from that is suitable for writing to file--- header, given an 'EntrySelector'.--getEntryName :: EntrySelector -> Text-getEntryName = unES-  >>> fmap CI.original-  >>> NE.intersperse "/"-  >>> NE.toList-  >>> concat-  >>> T.pack---- | The exception describing various troubles you can have with--- 'EntrySelector'.--data EntrySelectorException-  = InvalidEntrySelector (Path Rel File)-    -- ^ Selector cannot be created from this path-  deriving (Eq, Ord, Typeable)--instance Show EntrySelectorException where-  show (InvalidEntrySelector path) = "Cannot build selector from " ++ show path--instance Exception EntrySelectorException--------------------------------------------------------------------------------- Entry description---- | This record represents all 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 representation can be built given--- this data structure and the actual archive contents.--data EntryDescription = EntryDescription-  { edVersionMadeBy    :: Version -- ^ Version made by-  , edVersionNeeded    :: Version -- ^ Version needed to extract-  , edCompression      :: CompressionMethod -- ^ Compression method-  , edModTime          :: UTCTime -- ^ Last modification date and time-  , edCRC32            :: Word32  -- ^ CRC32 check sum-  , edCompressedSize   :: Natural -- ^ Size of compressed entry-  , edUncompressedSize :: Natural -- ^ Size of uncompressed entry-  , edOffset           :: Natural -- ^ Absolute offset of local file header-  , edComment          :: Maybe Text -- ^ Entry comment-  , edExtraField       :: Map Word16 ByteString -- ^ All extra fields found-  } deriving (Eq, Typeable)---- | Supported compression methods.--data CompressionMethod-  = Store              -- ^ Store file uncompressed-  | Deflate            -- ^ Deflate-  | BZip2              -- ^ Compressed using BZip2 algorithm-  deriving (Show, Read, Eq, Ord, Enum, Bounded, Data, Typeable)--------------------------------------------------------------------------------- Archive description---- | Information about archive as a whole.--data ArchiveDescription = ArchiveDescription-  { adComment  :: Maybe Text -- ^ Comment of entire archive-  , adCDOffset :: Natural -- ^ Absolute offset of start of central directory-  , adCDSize   :: Natural -- ^ Size of central directory record-  } deriving (Show, Read, Eq, Ord, Typeable, Data)--------------------------------------------------------------------------------- Exceptions---- | The bad things that can happen when you use the library.--data ZipException-  = EntryDoesNotExist (Path Abs File) EntrySelector-    -- ^ Thrown when you try to get contents of non-existing entry-  | ParsingFailed     (Path Abs File) String-    -- ^ Thrown when archive structure cannot be parsed-  deriving (Eq, Ord, Typeable)--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--instance Exception ZipException
+ Codec/Archive/Zip/Unix.hs view
@@ -0,0 +1,40 @@+-- |+-- Module      :  Codec.Archive.Zip.Unix+-- Copyright   :  © 2016–present Mark Karpov+-- License     :  BSD 3 clause+--+-- Maintainer  :  Mark Karpov <markkarpov92@gmail.com>+-- Stability   :  experimental+-- Portability :  portable+--+-- Unix-specific functionality of zip archives.+--+-- @since 1.4.0+module Codec.Archive.Zip.Unix+  ( toFileMode,+    fromFileMode,+  )+where++import Data.Bits+import Data.Word+import System.Posix.Types (CMode (..))++-- | Convert external attributes to the file info.+--+-- >>> toFileMode 2179792896+-- 0o0755+--+-- @since 1.4.0+toFileMode :: Word32 -> CMode+toFileMode attrs = fromIntegral $ (attrs `shiftR` 16) .&. 0x0fff++-- | Convert external attributes to the file info. The function assumes a+-- regular file and keeps DOS attributes untouched.+--+-- >>> fromFileMode 0o0755+-- 2179792896+--+-- @since 1.4.0+fromFileMode :: CMode -> Word32+fromFileMode cmode = (0o100000 .|. fromIntegral cmode) `shiftL` 16
LICENSE.md view
@@ -1,4 +1,4 @@-Copyright © 2016–2017 Mark Karpov+Copyright © 2016–present Mark Karpov  All rights reserved. 
README.md view
@@ -4,10 +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)-[![Build Status](https://travis-ci.org/mrkkrp/zip.svg?branch=master)](https://travis-ci.org/mrkkrp/zip)-[![Coverage Status](https://coveralls.io/repos/mrkkrp/zip/badge.svg?branch=master&service=github)](https://coveralls.io/github/mrkkrp/zip?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)@@ -23,42 +22,35 @@ * [License](#license)  This is a feature-rich, memory-efficient, and type-safe library to-manipulate Zip archives in Haskell. The library is the most complete and-efficient implementation of .ZIP specification in pure Haskell (at least-from open-sourced ones). In particular, it's 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.+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 all the functionality a user may need (obviously-the libraries provide functionality that their authors needed) and otherwise-is flawed in some way so it cannot be easily used in some situations. Let's-examine all the libraries available on Hackage to understand motivation for-this package.+provides only a subset of useful functionality or otherwise is flawed in+some way so it cannot be easily used in some situations. Let's examine all+libraries available on Hackage to understand the motivation for this+package.  ### zip-archive  `zip-archive` is a widely used library. It's quite old, well-known and-simple to use. However it creates Zip archives purely, as `ByteStrings`s in-memory that you can then write to the file system. This is not acceptable if-you work with more-or-less big data. For example, if you have collection of-files with total size of 500 MB and you want to pack them into an archive,-you can easily consume up to 1 GB of memory (the files plus resulting-archive). Not always you can afford to do this or do this at scale. Even if-you want just to look at list of archive entries it will read it into memory-in all its entirety. For my use-case it's not acceptable.+simple to use. However, it creates Zip archives purely, as `ByteStrings`s in+memory. This is not acceptable if you work with big data. For example, if+you have a collection of files with the total size 500 MB and you want to+pack them into an archive, you can easily consume up to 1 GB of memory (the+files plus the resulting archive). This is not always affordable. Even if+you want just to look at the list of archive entries it will read the entire+archive into memory.  ### LibZip -This is a binding to C-library [`libzip`](https://en.wikipedia.org/wiki/Libzip). There is always a-certain kind of trouble when you are using bindings. For example, you need-to take care that target library is installed and its version is compatible-with the version of your binding. Yes, this means additional headaches. It-should be just “plug and play”, but now you need to watch out for-compatibility.+This is a binding to the C library [`libzip`][libzip]. There is always a+certain kind of trouble with bindings. For example, you need to ensure that+the target library is installed and its version is compatible with the+version of your binding.  It's not that bad with libraries that do not break their API for years, but it's not the case with `libzip`. As the maintainer of `LibZip` puts it:@@ -67,40 +59,40 @@ > 0.11.x, then you should use LibZip 0.11. If your C library is 1.0, then > you should use LibZip master branch (not yet released to Hackage). -Now, on my machine I have version 1.0. To put the package on Stackage we had-to use version 0.10, because Stackage uses Ubuntu to build packages and-libraries on Ubuntu are always ancient. This means that I cannot use the+Now, on my machine I have the version 1.0. To put the package on Stackage we+had to use the version 0.10, because Stackage uses Ubuntu to build packages+and libraries on Ubuntu are always ancient. This means that I cannot use the version of the library from Stackage, and I don't yet know what will be on the server. -After much frustration with all these things I decided to avoid using of-`LibZip`, because after all, this is not that sort of a project that-shouldn't be done in pure Haskell. By rewriting this in Haskell, I also can-make it safer to use.+After much frustration, I decided to avoid using `LibZip`. After all, this+is not a project that shouldn't be done completely in Haskell. By rewriting+this in Haskell, I also can make it safer to use.  ### zip-conduit  This one uses the right approach: leverage a good streaming library-(`conduit`) for memory-efficient processing. This is however is not-feature-rich and has certain problems (including programming style, it uses-`error` if an entry is missing in archive, among other things), some of them-are reported on its issue tracker. It also does not appear to be maintained-(the last sign of activity was on December 23, 2014).+(`conduit`) for memory-efficient processing. The library is however not+feature-rich and has certain problems (including the programming style, it+uses `error` if an entry is missing in the archive, among other things),+some of them are reported on its issue tracker. It also does not appear to+be maintained (the last sign of activity was on December 23, 2014).  ## Features  The library supports all features specified in the modern .ZIP specification except for encryption and multi-disk archives. See more about this below. -For reference, here is a [copy of the specification](https://pkware.cachefly.net/webdocs/APPNOTE/APPNOTE-6.3.3.TXT).+For reference, here is a [copy of the specification][specification].  ### Compression methods  `zip` supports the following compression methods:  * Store (no compression, just store files “as is”)-* [DEFLATE](https://en.wikipedia.org/wiki/DEFLATE)-* [Bzip2](https://en.wikipedia.org/wiki/Bzip2)+* [DEFLATE]+* [Bzip2]+* [Zstandard]  The best way to add a new compression method to the library is to write a conduit that will do the compression and publish it as a library. `zip` can@@ -118,12 +110,11 @@  ### Sources of file data -The library gives you many options how to get file contents to compress and-how to get extracted data. The following methods are supported:+The following sources are supported:  * *File name.* This is an efficient method to perform compression or-  decompression. You just specify where to get data or where to save it and-  the rest is handled by the library.+  decompression. You specify where to get data or where to save it and the+  rest is handled by the library. * *Conduit source or sink.* * *ByteString.* Use it only with small data. * *Copy file from another archive.* An efficient operation, file is copied@@ -132,62 +123,60 @@ ### ZIP64  When necessary, the `ZIP64` extension is automatically used. It's necessary-when anything from this list takes place:+when: -* Total size of archive is greater than 4 GB.-* Size of compressed/uncompressed file in archive is greater than 4 GB.-* There are more than 65535 entries in archive.+* The total size of the archive is greater than 4 GB.+* The size of a single compressed/uncompressed file in the archive is+  greater than 4 GB.+* There are more than 65535 entries in the archive. -The library is particularly suited for processing of large files. For-example, I've been able to easily create 6.5 GB archive with reasonable-speed and without significant memory consumption.+The library is particularly well suited for processing large files. For+example, I've been able to create 6.5 GB archive with reasonable speed and+without significant memory consumption.  ### Filenames -The library has API that makes it impossible to create archive with+The library has an API that makes it impossible to create archive with non-portable or invalid file names in it.  As of .ZIP specification 6.3.2, files with Unicode symbols in their names-can be put into Zip archives. The library supports mechanisms for this and+can be stored in Zip archives. The library supports mechanisms for this and uses them automatically when needed. Besides UTF-8, CP437 is also supported-as it's required in the specification.+as per the specification.  ### Meta-information about files -The library allows to attach comments to entire archive or individual files,-and also gives its user full control over extra fields that are written to-file headers, so the user can store arbitrary information about file in the-archive.+The library allows us to attach comments to the entire archive or individual+files, and also gives its user full control over extra fields that are+written to file headers, so the user can store arbitrary information about+files in the archive.  ## Quick start  The module `Codec.Archive.Zip` provides everything you may need to manipulate Zip archives. There are three things that should be clarified-right away, to avoid confusion in the future.+right away to avoid confusion. -First, we use the `EntrySelector` type that can be obtained from `Path Rel-File` paths. This method may seem awkward at first, but it will protect you-from the problems with portability when your archive is unpacked on a-different platform. Using well-typed paths is also something you should-consider doing in your projects anyway. Even if you don't want to use the-`Path` module in your project, it's easy to marshal `FilePath` to `Path`-just before using functions from the library.+First, we use the `EntrySelector` type that can be obtained from relative+`FilePath`s (paths to directories are not allowed). This method may seem+awkward at first, but it will protect you from the problems with portability+when your archive is unpacked on a different platform. -The second thing, that is rather a consequence of the first, is that there-is no way to add directories, or to be precise, *empty directories* to your-archive. This approach is used in Git, and I find it quite sane.+Second, there is no way to add directories, or to be precise, *empty+directories* to your archive. This approach is used in Git and I find it+sane. -Finally, the third feature of the library is that it does not modify archive-instantly, because doing so on every manipulation would often be-inefficient. Instead we maintain a collection of pending actions that can be-turned into an optimized procedure that efficiently modifies archive in one-pass. Normally this should be of no concern to you, because all actions are-performed automatically when you leave the realm of `ZipArchive` monad. If,-however, you ever need to force an update, the `commit` function is your-friend. There are even “undo” functions, by the way.+Finally, the third feature of the library is that it does not modify the+archive instantly, because doing so on every manipulation would often be+inefficient. Instead, we maintain a collection of pending actions that can+be turned into an optimized procedure that efficiently modifies the archive+in one pass. Normally, this should be of no concern to you, because all+actions are performed automatically when you leave the `ZipArchive` monad.+If, however, you ever need to force an update, the `commit` function is your+friend. -Let's take a look at some examples that show how to accomplish most typical-tasks with help of the library.+Let's take a look at some examples that show how to accomplish most common+tasks.  To get full information about archive entries, use `getEntries`: @@ -214,7 +203,7 @@ λ> withArchive archivePath (saveEntry entrySelector pathToFile) ``` -…and finally just unpack the entire archive into some directory:+…and finally just unpack the entire archive into a directory:  ```haskell λ> withArchive archivePath (unpackInto destDir)@@ -222,9 +211,9 @@  See also `getArchiveComment` and `getArchiveDescription`. -Modifying is also easy, efficient, and powerful. When you want to create a-new archive use `createArchive`, otherwise `withArchive` will do. To add an-entry from `ByteString`:+Modifying is also easy. When you want to create a new archive use+`createArchive`, otherwise `withArchive` will do. To add an entry from+`ByteString`:  ```haskell λ> createArchive archivePath (addEntry Store "Hello, World!" entrySelector)@@ -239,7 +228,7 @@ To add contents from a file, use `loadEntry`:  ```haskell-λ> let toSelector = const $ parseRelFile "my-entry.txt" >>= mkEntrySelector+λ> let toSelector = const (mkEntrySelector "my-entry.txt") λ> createArchive archivePath (loadEntry BZip2 toSelector myFilePath) ``` @@ -253,8 +242,7 @@ It's often desirable to just pack a directory:  ```haskell-λ> let f = stripDir dir >=> mkEntrySelector-λ> createArchive archivePath (packDirRecur Deflate f dir)+λ> createArchive archivePath (packDirRecur Deflate mkEntrySelector dir) ```  It's also possible to:@@ -275,13 +263,19 @@  ## Contribution -You can contact the maintainer via-[the issue tracker](https://github.com/mrkkrp/zip/issues).+You can contact the maintainer via [the issue+tracker](https://github.com/mrkkrp/zip/issues). -Pull requests are also welcome and will be reviewed quickly.+Pull requests are welcome.  ## License -Copyright © 2016–2017 Mark Karpov+Copyright © 2016–present Mark Karpov  Distributed under BSD 3 clause license.++[libzip]: https://en.wikipedia.org/wiki/Libzip+[specification]: https://pkware.cachefly.net/webdocs/APPNOTE/APPNOTE-6.3.3.TXT+[DEFLATE]: https://en.wikipedia.org/wiki/DEFLATE+[Bzip2]: https://en.wikipedia.org/wiki/Bzip2+[Zstandard]: https://en.wikipedia.org/wiki/Zstandard
− Setup.hs
@@ -1,6 +0,0 @@-module Main (main) where--import Distribution.Simple--main :: IO ()-main = defaultMain
+ bench-app/Main.hs view
@@ -0,0 +1,19 @@+module Main (main) where++import Codec.Archive.Zip+import System.Environment (getArgs)+import System.Exit (exitFailure)+import System.FilePath++main :: IO ()+main = do+  [operation, input, output] <- getArgs+  case operation of+    "compress" -> do+      selector <- mkEntrySelector (takeFileName input)+      createArchive output (loadEntry Deflate selector input)+    "uncompress" ->+      withArchive input (unpackInto output)+    _ -> do+      putStrLn "Unknown command."+      exitFailure
tests/Main.hs view
@@ -1,706 +1,939 @@-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TemplateHaskell   #-}-{-# OPTIONS -fno-warn-orphans  #-}--module Main (main) where--import Codec.Archive.Zip-import Codec.Archive.Zip.CP437-import Control.Monad-import Control.Monad.Catch (catchIOError)-import Control.Monad.IO.Class-import Data.Bits (complement)-import Data.ByteString (ByteString)-import Data.Conduit (yield)-import Data.Foldable (foldl')-import Data.List (intercalate)-import Data.Map (Map, (!))-import Data.Maybe (fromJust)-import Data.Monoid-import Data.Text (Text)-import Data.Time-import Data.Version-import Path-import Path.IO-import System.IO-import System.IO.Error (isDoesNotExistError)-import Test.Hspec-import Test.QuickCheck-import qualified Data.ByteString         as B-import qualified Data.ByteString.Builder as LB-import qualified Data.ByteString.Lazy    as LB-import qualified Data.Conduit.List       as CL-import qualified Data.Map                as M-import qualified Data.Set                as E-import qualified Data.Text               as T-import qualified Data.Text.Encoding      as T-import qualified System.FilePath.Windows as Windows---- | Zip tests. Please note that Zip64 feature is not currently tested--- automatically because for it to expose itself we need > 4GB of--- data. Handling such quantities of data locally is problematic and even--- more problematic in the context of CI server.--main :: IO ()-main = hspec $ do-  describe "mkEntrySelector" mkEntrySelectorSpec-  describe "unEntrySelector" unEntrySelectorSpec-  describe "getEntryName"    getEntryNameSpec-  describe "decodeCP437"     decodeCP437Spec-  around withSandbox $ do-    describe "createArchive"      createArchiveSpec-    describe "withArchive"        withArchiveSpec-    describe "archive comment"    archiveCommentSpec-    describe "getEntryDesc"       getEntryDescSpec-    describe "version needed"     versionNeededSpec-    describe "addEntry"           addEntrySpec-    describe "sinkEntry"          sinkEntrySpec-    describe "loadEntry"          loadEntrySpec-    describe "copyEntry"          copyEntrySpec-    describe "checkEntry"         checkEntrySpec-    describe "recompress"         recompressSpec-    describe "entry comment"      entryCommentSpec-    describe "setModTime"         setModTimeSpec-    describe "extra field"        extraFieldSpec-    describe "renameEntry"        renameEntrySpec-    describe "deleteEntry"        deleteEntrySpec-    describe "forEntries"         forEntriesSpec-    describe "undoEntryChanges"   undoEntryChangesSpec-    describe "undoArchiveChanges" undoArchiveChangesSpec-    describe "undoAll"            undoAllSpec-    describe "consistency"        consistencySpec-    describe "packDirRecur"       packDirRecurSpec-    describe "unpackInto"         unpackIntoSpec--------------------------------------------------------------------------------- Arbitrary instances and generators--instance Arbitrary Text where-  arbitrary = T.pack <$> listOf1 arbitrary--instance Arbitrary ByteString where-  arbitrary = B.pack <$> listOf arbitrary--instance Arbitrary CompressionMethod where-  arbitrary = elements [Store, Deflate, BZip2]--instance Arbitrary UTCTime where-  arbitrary = UTCTime-    <$> (ModifiedJulianDay <$> choose (44239, 90989))-    <*> (secondsToDiffTime <$> choose (0, 86399))--instance Arbitrary (Path Rel File) where-  arbitrary = do-    x <- intercalate "/" <$> listOf1 (listOf1 charGen)-    case parseRelFile x of-      Nothing -> arbitrary-      Just path -> return path--instance Arbitrary EntrySelector where-  arbitrary = do-    x <- arbitrary-    case mkEntrySelector x of-      Nothing -> arbitrary-      Just s  -> return s--data EM = EM EntrySelector EntryDescription (ZipArchive ()) deriving Show--instance Arbitrary EM where-  arbitrary = do-    s       <- arbitrary-    method  <- arbitrary-    content <- arbitrary-    modTime <- arbitrary-    comment <- arbitrary-    extraFieldTag <- arbitrary `suchThat` (/= 1)-    extraFieldContent <- arbitrary `suchThat` ((< 0xffff) . B.length)-    let action = do-          addEntry method content s-          setModTime modTime s-          setEntryComment comment s-          addExtraField extraFieldTag extraFieldContent s-    return $ EM s EntryDescription-      { edVersionMadeBy    = undefined-      , edVersionNeeded    = undefined-      , edCompression      = method-      , edModTime          = modTime-      , edCRC32            = undefined-      , edCompressedSize   = undefined-      , edUncompressedSize = fromIntegral (B.length content)-      , edOffset           = undefined-      , edComment          = Just comment-      , edExtraField       = M.singleton extraFieldTag extraFieldContent }-      action--data EC = EC (Map EntrySelector EntryDescription) (ZipArchive ()) deriving Show--instance Arbitrary EC where-  arbitrary = foldl' f (EC M.empty (return ())) <$> listOf1 arbitrary-    where f (EC m z') (EM s desc z) = EC (M.insert s desc m) (z' >> z)--charGen :: Gen Char-charGen = frequency-  [ (3, choose ('a', 'z'))-  , (3, choose ('A', 'Z'))-  , (3, choose ('0', '9'))-  , (1, arbitrary `suchThat` (>= ' ')) ]--binASCII :: Gen ByteString-binASCII = LB.toStrict . LB.toLazyByteString <$> go-  where go = frequency-          [ (10, (<>) <$> (LB.word8 <$> choose (0, 127)) <*> go)-          , (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) ++-    " }"--instance Show (ZipArchive a) where-  show = const "<zip archive>"--------------------------------------------------------------------------------- Pure operations and periphery--mkEntrySelectorSpec :: Spec-mkEntrySelectorSpec = do-  context "when incorrect Windows paths are passed" $-    it "rejects them" . property $ \path ->-      (not . Windows.isValid . toFilePath $ path)-        ==> mkEntrySelector path === Nothing-  context "when too long paths are passed" $-    it "rejects them" $ do-      path <- parseRelFile (replicate 0x10000 'a')-      mkEntrySelector path `shouldThrow` isEntrySelectorException path-  context "when correct paths are passed" $-    it "adequately represents them" $ do-      let c str = do-            s <- parseRelFile str >>= mkEntrySelector-            getEntryName s `shouldBe` T.pack str-      c "one/two/three"-      c "something.txt"--unEntrySelectorSpec :: Spec-unEntrySelectorSpec =-  context "when entry selector exists" $-    it "has corresponding path" . property $ \s ->-      not . null . toFilePath . unEntrySelector $ s--getEntryNameSpec :: Spec-getEntryNameSpec =-  context "when entry selector exists" $-    it "has corresponding representation" . property $ \s ->-      not . T.null . getEntryName $ s--decodeCP437Spec :: Spec-decodeCP437Spec = do-  context "when ASCII-compatible subset is used" $-    it "has the same result as decoding UTF-8" . property $-      forAll binASCII $ \bin ->-        decodeCP437 bin `shouldBe` T.decodeUtf8 bin-  context "when non-ASCII subset is used" $-    it "is decoded correctly" $ do-      let c b t = decodeCP437 (B.pack b ) `shouldBe` t-      c [0x80..0x9f] "ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒ"-      c [0xa0..0xbf] "áíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐"-      c [0xc0..0xdf] "└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀"-      c [0xe0..0xff] "αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ "--------------------------------------------------------------------------------- Primitive editing/querying actions--createArchiveSpec :: SpecWith (Path Abs File)-createArchiveSpec = do-  context "when called with non-existent path and empty recipe" $-    it "creates correct representation of empty archive" $ \path -> do-      createArchive path (return ())-      B.readFile (toFilePath path) `shouldReturn` emptyArchive-  context "when called with occupied path" $-    it "overwrites it" $ \path -> do-      B.writeFile (toFilePath path) B.empty-      createArchive path (return ())-      B.readFile (toFilePath path) `shouldNotReturn` B.empty--withArchiveSpec :: SpecWith (Path Abs File)-withArchiveSpec = do-  context "when called with non-existent path" $-    it "throws 'isDoesNotExistError' exception" $ \path ->-      withArchive path (return ()) `shouldThrow` isDoesNotExistError-  context "when called with occupied path (empty file)" $-    it "throws 'ParsingFailed' exception" $ \path -> do-      B.writeFile (toFilePath path) B.empty-      withArchive path (return ()) `shouldThrow`-        isParsingFailed path "Cannot locate end of central directory"-  context "when called with occupied path (empty archive)" $-    it "does not overwrite the file unnecessarily" $ \path -> do-      let fp = toFilePath path-      B.writeFile fp emptyArchive-      withArchive path . liftIO $ B.writeFile fp B.empty-      B.readFile fp `shouldNotReturn` emptyArchive--archiveCommentSpec :: SpecWith (Path Abs File)-archiveCommentSpec = do-  context "when new archive is created" $-    it "returns no archive comment" $ \path ->-      createArchive path getArchiveComment `shouldReturn` Nothing-  context "when comment contains end of central directory signature" $-    it "reads it without problems" $ \path -> do-      entries <- createArchive path $ do-        setArchiveComment "I saw you want to have PK\ENQ\ACK here."-        commit-        getEntries-      entries `shouldBe` M.empty-  context "when comment is committed (delete/set)" $-    it "reads it and updates" $ \path -> property $ \txt -> do-      comment <- createArchive path $ do-        deleteArchiveComment-        setArchiveComment txt-        commit-        getArchiveComment-      comment `shouldBe` Just txt-  context "when comment is committed (set/delete)" $-    it "reads it and updates" $ \path -> property $ \txt -> do-      comment <- createArchive path $ do-        setArchiveComment txt-        deleteArchiveComment-        commit-        getArchiveComment-      comment `shouldBe` Nothing-  context "when pre-existing comment is overwritten" $-    it "returns the new comment" $ \path -> property $ \txt txt' -> do-      comment <- createArchive path $ do-        setArchiveComment txt-        commit-        setArchiveComment txt'-        commit-        getArchiveComment-      comment `shouldBe` Just txt'-  context "when pre-existing comment is deleted" $-    it "actually deletes it" $ \path -> property $ \txt -> do-      comment <- createArchive path $ do-        setArchiveComment txt-        commit-        deleteArchiveComment-        commit-        getArchiveComment-      comment `shouldBe` Nothing--getEntryDescSpec :: SpecWith (Path Abs File)-getEntryDescSpec =-  it "always returns correct description" $-    \path -> property $ \(EM s desc z) -> do-      desc' <- fromJust <$> createArchive path (z >> commit >> getEntryDesc s)-      desc' `shouldSatisfy` softEq desc--versionNeededSpec :: SpecWith (Path Abs File)-versionNeededSpec =-  it "writes correct version that is needed to extract archive" $-    -- NOTE for now we check only how version depends on compression method,-    -- it should be mentioned that the version also depends on Zip64 feature-    \path -> 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])--addEntrySpec :: SpecWith (Path Abs File)-addEntrySpec =-  context "when an entry is added" $-    it "is there" $ \path -> property $ \m b s -> do-      info <- createArchive path $ do-        addEntry m b s-        commit-        (,) <$> getEntry s <*> (edCompression . (! s) <$> getEntries)-      info `shouldBe` (b, m)--sinkEntrySpec :: SpecWith (Path Abs File)-sinkEntrySpec =-  context "when an entry is sunk" $-    it "is there" $ \path -> property $ \m b s -> do-      info <- createArchive path $ do-        sinkEntry m (yield b) s-        commit-        (,) <$> sourceEntry s (CL.foldMap id)-          <*> (edCompression . (! s) <$> getEntries)-      info `shouldBe` (b, m)--loadEntrySpec :: SpecWith (Path Abs File)-loadEntrySpec =-  context "when an entry is loaded" $-    it "is there" $ \path -> property $ \m b s -> do-      let vpath = deriveVacant path-      B.writeFile (toFilePath vpath) b-      createArchive path $ do-        loadEntry m (const $ return s) vpath-        commit-        liftIO (removeFile vpath)-        saveEntry s vpath-      B.readFile (toFilePath vpath) `shouldReturn` b--copyEntrySpec :: SpecWith (Path Abs File)-copyEntrySpec =-  context "when entry is copied form another archive" $-    it "is there" $ \path -> property $ \m b s -> do-      let vpath = deriveVacant path-      createArchive vpath (addEntry m b s)-      info <- createArchive path $ do-        copyEntry vpath s s-        commit-        (,) <$> getEntry s <*> (edCompression . (! s) <$> getEntries)-      info `shouldBe` (b, m)--checkEntrySpec :: SpecWith (Path Abs File)-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 r = 50 + (B.length . T.encodeUtf8 . getEntryName $ s)-        offset <- createArchive path $ do-          addEntry Store b s-          commit-          fromIntegral . edOffset . (! s) <$> getEntries-        withFile (toFilePath path) ReadWriteMode $ \h -> do-          hSeek h AbsoluteSeek (offset + fromIntegral r)-          byte <- B.map complement <$> B.hGet h 1-          hSeek h RelativeSeek (-1)-          B.hPut h byte-        withArchive path (checkEntry s) `shouldReturn` False--recompressSpec :: SpecWith (Path Abs File)-recompressSpec =-  context "when recompression is used" $-    it "gets recompressed" $ \path -> property $ \m m' b s -> do-      info <- createArchive path $ do-        addEntry m b s-        commit-        recompress m' s-        commit-        (,) <$> getEntry s <*> (edCompression . (! s) <$> getEntries)-      info `shouldBe` (b, m')--entryCommentSpec :: SpecWith (Path Abs File)-entryCommentSpec = do-  context "when comment is committed (delete/set)" $-    it "reads it and updates" $ \path -> property $ \txt s -> do-      comment <- createArchive path $ do-        addEntry Store "foo" s-        deleteEntryComment s-        setEntryComment txt s-        commit-        edComment . (! s) <$> getEntries-      comment `shouldBe` Just txt-  context "when comment is committed (set/delete)" $-    it "reads it and updates" $ \path -> property $ \txt s -> do-      comment <- createArchive path $ do-        addEntry Store "foo" s-        setEntryComment txt s-        deleteEntryComment s-        commit-        edComment . (! s) <$> getEntries-      comment `shouldBe` Nothing-  context "when pre-existing comment is overwritten" $-    it "returns the new comment" $ \path -> property $ \txt txt' s -> do-      comment <- createArchive path $ do-        addEntry Store "foo" s-        setEntryComment txt s-        commit-        setEntryComment txt' s-        commit-        edComment . (! s) <$> getEntries-      comment `shouldBe` Just txt'-  context "when pre-existing comment is deleted" $-    it "actually deletes it" $ \path -> property $ \txt s -> do-      comment <- createArchive path $ do-        addEntry Store "foo" s-        setEntryComment txt s-        commit-        deleteEntryComment s-        commit-        edComment . (! s) <$> getEntries-      comment `shouldBe` Nothing--setModTimeSpec :: SpecWith (Path Abs File)-setModTimeSpec = do-  context "when mod time is set (after creation)" $-    it "reads it and updates" $ \path -> property $ \time s -> do-      modTime <- createArchive path $ do-        addEntry Store "foo" s-        setModTime time s-        commit-        edModTime . (! s) <$> getEntries-      modTime `shouldSatisfy` isCloseTo time-  context "when mod time is set (before creation)" $-    it "has no effect" $ \path -> property $ \time time' s ->-      not (isCloseTo time time') ==> do-        modTime <- createArchive path $ do-          setModTime time s-          addEntry Store "foo" s-          commit-          edModTime . (! s) <$> getEntries-        modTime `shouldNotSatisfy` isCloseTo time--extraFieldSpec :: SpecWith (Path Abs File)-extraFieldSpec = do-  context "when extra field is committed (delete/set)" $-    it "reads it and updates" $ \path -> property $ \n b s ->-      n /= 1 ==> do-        efield <- createArchive path $ do-          addEntry Store "foo" s-          deleteExtraField n s-          addExtraField n b s-          commit-          M.lookup n . edExtraField . (! s) <$> getEntries-        efield `shouldBe` Just b-  context "when extra field is committed (set/delete)" $-    it "reads it and updates" $ \path -> property $ \n b s ->-      n /= 1 ==> do-        efield <- createArchive path $ do-          addEntry Store "foo" s-          addExtraField n b s-          deleteExtraField n s-          commit-          M.lookup n . edExtraField . (! s) <$> getEntries-        efield `shouldBe` Nothing-  context "when pre-existing extra field is overwritten" $-    it "reads it and updates" $ \path -> property $ \n b b' s ->-      n /= 1 ==> do-        efield <- createArchive path $ do-          addEntry Store "foo" s-          addExtraField n b s-          commit-          addExtraField n b' s-          commit-          M.lookup n . edExtraField . (! s) <$> getEntries-        efield `shouldBe` Just b'-  context "when pre-existing extra field is deleted" $-    it "actually deletes it" $ \path -> property $ \n b s ->-      n /= 1 ==> do-        efield <- createArchive path $ do-          addEntry Store "foo" s-          addExtraField n b s-          commit-          deleteExtraField n s-          commit-          M.lookup n . edExtraField . (! s) <$> getEntries-        efield `shouldBe` Nothing--renameEntrySpec :: SpecWith (Path Abs File)-renameEntrySpec = do-  context "when renaming after editing of new entry" $-    it "produces correct result" $ \path -> property $ \(EM s desc z) s' -> do-      desc' <- createArchive path $ do-        z-        renameEntry s s'-        commit-        (! s') <$> getEntries-      desc' `shouldSatisfy` softEq desc-  context "when renaming existing entry" $-    it "gets renamed" $ \path -> property $ \(EM s desc z) s' -> do-      desc' <- createArchive path $ do-        z-        commit-        renameEntry s s'-        commit-        (! s') <$> getEntries-      desc' `shouldSatisfy` softEq desc--deleteEntrySpec :: SpecWith (Path Abs File)-deleteEntrySpec = do-  context "when deleting after editing of new entry" $-    it "produces correct result" $ \path -> property $ \(EM s _ z) -> do-      member <- createArchive path $ do-        z-        deleteEntry s-        commit-        doesEntryExist s-      member `shouldBe` False-  context "when deleting existing entry" $-    it "gets deleted" $ \path -> property $ \(EM s _ z) -> do-      member <- createArchive path $ do-        z-        commit-        deleteEntry s-        commit-        doesEntryExist s-      member `shouldBe` False--forEntriesSpec :: SpecWith (Path Abs File)-forEntriesSpec =-  it "affects all existing entries" $ \path -> property $ \(EC m z) txt -> do-    m' <- createArchive path $ do-      z-      commit-      forEntries (setEntryComment txt)-      commit-      getEntries-    let f ed = ed { edComment = Just txt }-    m' `shouldSatisfy` softEqMap (M.map f m)--undoEntryChangesSpec :: SpecWith (Path Abs File)-undoEntryChangesSpec =-  it "cancels all actions for specified entry" $-    \path -> property $ \(EM s _ z) -> do-      member <- createArchive path $ do-        z-        undoEntryChanges s-        commit-        doesEntryExist s-      member `shouldBe` False--undoArchiveChangesSpec :: SpecWith (Path Abs File)-undoArchiveChangesSpec = do-  it "cancels archive comment editing" $ \path -> property $ \txt -> do-    comment <- createArchive path $ do-      setArchiveComment txt-      undoArchiveChanges-      commit-      getArchiveComment-    comment `shouldBe` Nothing-  it "cancels archive comment deletion" $ \path -> property $ \txt -> do-    comment <- createArchive path $ do-      setArchiveComment txt-      commit-      deleteArchiveComment-      undoArchiveChanges-      commit-      getArchiveComment-    comment `shouldBe` Just txt--undoAllSpec :: SpecWith (Path Abs File)-undoAllSpec =-  it "cancels all editing at once" $ \path -> property $ \(EC _ z) txt -> do-    let fp = toFilePath path-    createArchive path (return ())-    withArchive path $ do-      z-      setArchiveComment txt-      undoAll-      liftIO (B.writeFile fp B.empty)-    B.readFile fp `shouldReturn` B.empty--------------------------------------------------------------------------------- Complex construction/restoration--consistencySpec :: SpecWith (Path Abs File)-consistencySpec =-  it "can save and restore arbitrary archive" $-    \path -> property $ \(EC m z) txt -> do-      (txt', m') <- createArchive path $ do-        z-        setArchiveComment txt-        commit-        (,) <$> getArchiveComment <*> getEntries-      txt' `shouldBe` Just txt-      m' `shouldSatisfy` softEqMap m--packDirRecurSpec :: SpecWith (Path Abs File)-packDirRecurSpec =-  it "packs arbitrary directory recursively" $-    \path -> property $ \contents -> do-      let dir = parent path-          f   = stripDir dir >=> mkEntrySelector-      blew <- catchIOError (do-        forM_ contents $ \s -> do-          let item = dir </> unEntrySelector s-          ensureDir (parent item)-          B.writeFile (toFilePath item) "foo"-        return False)-        (const $ return True)-      when blew discard -- TODO-      selectors <- M.keysSet <$>-        createArchive path (packDirRecur Store f dir >> commit >> getEntries)-      selectors `shouldBe` E.fromList contents--unpackIntoSpec :: SpecWith (Path Abs File)-unpackIntoSpec =-  it "unpacks archive contents into directory" $-    \path -> property $ \(EC m z) -> do-      let dir = parent path-      blew <- createArchive path $ do-        z-        commit-        catchIOError-          (unpackInto dir >> return False)-          (const $ return True)-      when blew discard -- TODO-      removeFile path-      selectors <- listDirRecur dir >>=-        mapM (stripDir dir >=> mkEntrySelector) . snd-      E.fromList selectors `shouldBe` M.keysSet m--------------------------------------------------------------------------------- Helpers---- | Check whether given exception is 'EntrySelectorException' with specific--- path inside.--isEntrySelectorException :: Path Rel File -> EntrySelectorException -> Bool-isEntrySelectorException path (InvalidEntrySelector p) = p == path---- | Check whether given exception is 'ParsingFailed' exception with--- specific path and error message inside.--isParsingFailed :: Path Abs File -> String -> ZipException -> Bool-isParsingFailed path msg (ParsingFailed path' msg') =-  path == path' && msg == msg'-isParsingFailed _ _ _ = False---- | Create sandbox directory to model some situation in it and run some--- tests. Note that we're using new unique sandbox directory for each test--- case to avoid contamination and it's unconditionally deleted after test--- case finishes. The function returns vacant file path in that directory.--withSandbox :: ActionWith (Path Abs File) -> IO ()-withSandbox action = withSystemTempDir "zip-sandbox" $ \dir ->-  action (dir </> $(mkRelFile "foo.zip"))---- | Given primary name (name of archive), generate a name that does not--- collide with it.--deriveVacant :: Path Abs File -> Path Abs File-deriveVacant = (</> $(mkRelFile "bar")) . parent---- | Compare times forgiving minor difference.--isCloseTo :: UTCTime -> UTCTime -> Bool-isCloseTo a b = abs (diffUTCTime a b) < 2---- | Compare only some fields of 'EntryDescription' record.--softEq :: EntryDescription -> EntryDescription -> Bool-softEq a b =-  edCompression a == edCompression b &&-  isCloseTo (edModTime a) (edModTime b) &&-  edUncompressedSize a == edUncompressedSize b &&-  edComment a == edComment b &&-  M.delete 1 (edExtraField a) == M.delete 1 (edExtraField b)---- | Compare two maps describing archive entries in such a way that only--- some fields in 'EntryDescription' record are tested.--softEqMap-  :: Map EntrySelector EntryDescription-  -> Map EntrySelector EntryDescription-  -> Bool-softEqMap n m = M.null (M.differenceWith f n m)-  where f a b = if softEq a b then Nothing else Just a---- | Canonical representation of empty Zip archive.--emptyArchive :: ByteString-emptyArchive = B.pack-  [ 0x50, 0x4b, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00-  , 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ]+{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Main (main) where++import Codec.Archive.Zip+import Codec.Archive.Zip.CP437+import Codec.Archive.Zip.Unix+import Control.Monad+import Control.Monad.IO.Class+import Data.Bits+import Data.ByteString (ByteString)+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 Data.Map.Strict qualified as M+import Data.Maybe (fromJust)+import Data.Set qualified as E+import Data.Text (Text)+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 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.+main :: IO ()+main = hspec $ do+  describe "mkEntrySelector" mkEntrySelectorSpec+  describe "unEntrySelector" unEntrySelectorSpec+  describe "getEntryName" getEntryNameSpec+  describe "decodeCP437" decodeCP437Spec+  describe "fromFileMode" fromFileModeSpec+  around withSandbox $ do+    describe "createArchive" createArchiveSpec+    describe "withArchive" withArchiveSpec+    describe "archive comment" archiveCommentSpec+    describe "getEntryDesc" getEntryDescSpec+    describe "version needed" versionNeededSpec+    describe "addEntry" addEntrySpec+    describe "sinkEntry" sinkEntrySpec+    describe "loadEntry" loadEntrySpec+    describe "copyEntry" copyEntrySpec+    describe "checkEntry" checkEntrySpec+    describe "recompress" recompressSpec+    describe "entry comment" entryCommentSpec+    describe "setModTime" setModTimeSpec+    describe "extra field" extraFieldSpec+    describe "setExternalFileAttrsSpec" setExternalFileAttrsSpec+    describe "renameEntry" renameEntrySpec+    describe "deleteEntry" deleteEntrySpec+    describe "forEntries" forEntriesSpec+    describe "undoEntryChanges" undoEntryChangesSpec+    describe "undoArchiveChanges" undoArchiveChangesSpec+    describe "undoAll" undoAllSpec+    describe "consistency" consistencySpec+    describe "packDirRecur'" packDirRecur'Spec+    describe "unpackInto" unpackIntoSpec++----------------------------------------------------------------------------+-- Arbitrary instances and generators++instance Arbitrary Text where+  arbitrary = T.pack <$> listOf1 arbitrary++instance Arbitrary ByteString where+  arbitrary = B.pack <$> scale (* 10) (listOf arbitrary)++{- ORMOLU_DISABLE -}++instance Arbitrary CompressionMethod where+  arbitrary =+    elements+      [ Store,+#ifdef ENABLE_BZIP2+        BZip2,+#endif+#ifdef ENABLE_ZSTD+        Zstd,+#endif+        Deflate+      ]++{- ORMOLU_ENABLE -}++instance Arbitrary UTCTime where+  arbitrary =+    UTCTime+      <$> (ModifiedJulianDay <$> choose (44239, 90989))+      <*> (secondsToDiffTime <$> choose (0, 86399))++newtype RelPath = RelPath FilePath++instance Show RelPath where+  show (RelPath path) = show path++instance Arbitrary RelPath where+  arbitrary = do+    p <-+      resize 10 $+        intercalate "/"+          <$> listOf1+            ( (++)+                <$> vectorOf 3 charGen+                <*> listOf1 charGen+            )+    case mkEntrySelector p of+      Nothing -> arbitrary+      Just _ -> return (RelPath p)++instance Arbitrary EntrySelector where+  arbitrary = do+    RelPath x <- arbitrary+    case mkEntrySelector x of+      Nothing -> arbitrary+      Just s -> return s++data EM = EM EntrySelector EntryDescription (ZipArchive ()) deriving (Show)++instance Arbitrary EM where+  arbitrary = do+    s <- arbitrary+    method <- arbitrary+    content <- arbitrary+    modTime <- arbitrary+    comment <- arbitrary+    externalFileAttrs <- arbitrary+    extraFieldTag <- arbitrary `suchThat` (/= 1)+    extraFieldContent <- arbitrary `suchThat` ((< 0xffff) . B.length)+    let action = do+          addEntry method content s+          setModTime modTime s+          setEntryComment comment s+          addExtraField extraFieldTag extraFieldContent s+          setExternalFileAttrs externalFileAttrs s+    return $+      EM+        s+        EntryDescription+          { edVersionMadeBy = undefined,+            edVersionNeeded = undefined,+            edCompression = method,+            edModTime = modTime,+            edCRC32 = undefined,+            edCompressedSize = undefined,+            edUncompressedSize = fromIntegral (B.length content),+            edOffset = undefined,+            edComment = Just comment,+            edExtraField = M.singleton extraFieldTag extraFieldContent,+            edExternalFileAttrs = externalFileAttrs+          }+        action++data EC = EC (Map EntrySelector EntryDescription) (ZipArchive ()) deriving (Show)++instance Arbitrary EC where+  arbitrary = do+    let f (EM s d z) = (s, (d, z))+    m <- M.fromList . fmap f <$> downScale (listOf arbitrary)+    return (EC (M.map fst m) (sequence_ $ snd <$> M.elems m))++charGen :: Gen Char+charGen =+  frequency+    [ (3, choose ('a', 'z')),+      (3, choose ('A', 'Z')),+      (3, choose ('0', '9')),+      (1, arbitrary `suchThat` (>= ' '))+    ]++binASCII :: Gen ByteString+binASCII = LB.toStrict . LB.toLazyByteString <$> go+  where+    go =+      frequency+        [ (10, (<>) <$> (LB.word8 <$> choose (0, 127)) <*> go),+          (1, return mempty)+        ]++instance Show (ZipArchive a) where+  show = const "<zip archive>"++----------------------------------------------------------------------------+-- Pure operations and periphery++mkEntrySelectorSpec :: Spec+mkEntrySelectorSpec = do+  let rejects x =+        mkEntrySelector x `shouldThrow` isEntrySelectorException x+      accepts x = do+        s <- mkEntrySelector x+        getEntryName s `shouldBe` T.pack x+  context "when absolute paths are passed" $+    it "they are rejected" $+      property $ \(RelPath x) ->+        rejects ('/' : x)+  context "when paths with trailing path separator are passed" $+    it "they are rejected" $ do+      rejects "foo/"+      rejects "foo/bar/"+  context "when paths with dot as path segment are passed" $+    it "they are rejected" $ do+      rejects "./foo/bar"+      rejects "foo/./bar"+      rejects "foo/bar/."+  context "when paths with double dot as path segment are passed" $+    it "they are rejected" $ do+      rejects "../foo/bar"+      rejects "foo/../bar"+      rejects "foo/bar/.."+  context "when too long paths are passed" $+    it "rejects them" $ do+      let path = replicate 0x10000 'a'+      mkEntrySelector path `shouldThrow` isEntrySelectorException path+  context "when correct paths are passed" $+    it "adequately represents them" $ do+      accepts "foo"+      accepts "one/two/three"+      accepts "something.txt"++unEntrySelectorSpec :: Spec+unEntrySelectorSpec =+  context "when entry selector exists" $+    it "has corresponding path" . property $ \s ->+      not . null . unEntrySelector $ s++getEntryNameSpec :: Spec+getEntryNameSpec =+  context "when entry selector exists" $+    it "has corresponding representation" . property $ \s ->+      not . T.null . getEntryName $ s++decodeCP437Spec :: Spec+decodeCP437Spec = do+  context "when ASCII-compatible subset is used" $+    it "has the same result as decoding UTF-8" . property $+      forAll binASCII $ \bin ->+        decodeCP437 bin `shouldBe` T.decodeUtf8 bin+  context "when non-ASCII subset is used" $+    it "is decoded correctly" $ do+      let c b t = decodeCP437 (B.pack b) `shouldBe` t+      c [0x80 .. 0x9f] "ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒ"+      c [0xa0 .. 0xbf] "áíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐"+      c [0xc0 .. 0xdf] "└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀"+      c [0xe0 .. 0xff] "αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ "++fromFileModeSpec :: Spec+fromFileModeSpec =+  context "UNIX helpers" $ do+    it "toFileMode . fromFileMode == id .&. 0x0fffff" . property $ \mode ->+      (toFileMode . fromFileMode) (fromIntegral mode)+        == fromIntegral (mode .&. (0x0fff :: Word16))+    it "toFileMode == toFileMode . fromFileMode . toFileMode" . property $ \mode ->+      toFileMode mode == (toFileMode . fromFileMode . toFileMode) mode++----------------------------------------------------------------------------+-- Primitive editing/querying actions++createArchiveSpec :: SpecWith FilePath+createArchiveSpec = do+  context "when called with non-existent path and empty recipe" $+    it "creates correct representation of empty archive" $ \path -> do+      createArchive path (return ())+      B.readFile path `shouldReturn` emptyArchive+  context "when called with an occupied path" $+    it "overwrites it" $ \path -> do+      B.writeFile path B.empty+      createArchive path (return ())+      B.readFile path `shouldNotReturn` B.empty++withArchiveSpec :: SpecWith FilePath+withArchiveSpec = do+  context "when called with non-existent path" $+    it "throws 'isDoesNotExistError' exception" $ \path ->+      withArchive path (return ()) `shouldThrow` isDoesNotExistError+  context "when called with occupied path (empty file)" $+    it "throws 'ParsingFailed' exception" $ \path -> do+      B.writeFile path B.empty+      withArchive path (return ())+        `shouldThrow` isParsingFailed path "Cannot locate end of central directory"+  context "when called with occupied path (empty archive)" $+    it "does not overwrite the file unnecessarily" $ \path -> do+      B.writeFile path emptyArchive+      withArchive path $+        liftIO $+          B.writeFile path B.empty+      B.readFile path `shouldNotReturn` emptyArchive++archiveCommentSpec :: SpecWith FilePath+archiveCommentSpec = do+  context "when new archive is created" $+    it "returns no archive comment" $ \path ->+      createArchive path getArchiveComment `shouldReturn` Nothing+  context "when comment contains end of central directory signature" $+    it "reads it without problems" $ \path -> do+      entries <- createArchive path $ do+        setArchiveComment "I saw you want to have PK\ENQ\ACK here."+        commit+        getEntries+      entries `shouldBe` M.empty+  context "when comment is committed (delete/set)" $+    it "reads it and updates" $ \path ->+      property $ \txt -> do+        comment <- createArchive path $ do+          deleteArchiveComment+          setArchiveComment txt+          commit+          getArchiveComment+        comment `shouldBe` Just txt+  context "when comment is committed (set/delete)" $+    it "reads it and updates" $ \path ->+      property $ \txt -> do+        comment <- createArchive path $ do+          setArchiveComment txt+          deleteArchiveComment+          commit+          getArchiveComment+        comment `shouldBe` Nothing+  context "when pre-existing comment is overwritten" $+    it "returns the new comment" $ \path ->+      property $ \txt txt' -> do+        comment <- createArchive path $ do+          setArchiveComment txt+          commit+          setArchiveComment txt'+          commit+          getArchiveComment+        comment `shouldBe` Just txt'+  context "when pre-existing comment is deleted" $+    it "actually deletes it" $ \path ->+      property $ \txt -> do+        comment <- createArchive path $ do+          setArchiveComment txt+          commit+          deleteArchiveComment+          commit+          getArchiveComment+        comment `shouldBe` Nothing++getEntryDescSpec :: SpecWith FilePath+getEntryDescSpec =+  it "always returns correct description" $ \path ->+    property $ \(EM s desc z) -> do+      desc' <- fromJust <$> createArchive path (z >> commit >> getEntryDesc s)+      desc' `shouldSatisfy` softEq desc++versionNeededSpec :: SpecWith FilePath+versionNeededSpec =+  it "writes correct version that is needed to extract archive" $ \path ->+    -- NOTE for now we check only how version depends on compression method,+    -- 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)+      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" $ 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 =+  context "when an entry is sunk" $+    it "is there" $ \path ->+      property $ \m b s -> do+        info <- createArchive path $ do+          sinkEntry m (C.yield b) s+          commit+          checkEntry' s+          (,)+            <$> sourceEntry s (CL.foldMap id)+            <*> (edCompression . (! s) <$> getEntries)+        info `shouldBe` (b, m)++loadEntrySpec :: SpecWith FilePath+loadEntrySpec =+  context "when an entry is loaded" $+    it "is there" $ \path ->+      property $ \m b s t -> do+        let vpath = deriveVacant path+        B.writeFile vpath b+        setModificationTime vpath t+        createArchive path $ do+          loadEntry m s vpath+          commit+          checkEntry' s+          liftIO (removeFile vpath)+          saveEntry s vpath+        B.readFile vpath `shouldReturn` b+        modTime <- getModificationTime vpath+        modTime `shouldSatisfy` isCloseTo t++copyEntrySpec :: SpecWith FilePath+copyEntrySpec =+  context "when entry is copied from another archive" $ do+    it "is there" $ \path ->+      property $ \m b s -> do+        let vpath = deriveVacant path+        createArchive vpath (addEntry m b s)+        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 "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+            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 =+  context "when recompression is used" $+    it "gets recompressed" $ \path ->+      property $ \m m' b s -> do+        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')++entryCommentSpec :: SpecWith FilePath+entryCommentSpec = do+  context "when comment is committed (delete/set)" $+    it "reads it and updates" $ \path ->+      property $ \txt s -> do+        comment <- createArchive path $ do+          addEntry Store "foo" s+          deleteEntryComment s+          setEntryComment txt s+          commit+          edComment . (! s) <$> getEntries+        comment `shouldBe` Just txt+  context "when comment is committed (set/delete)" $+    it "reads it and updates" $ \path ->+      property $ \txt s -> do+        comment <- createArchive path $ do+          addEntry Store "foo" s+          setEntryComment txt s+          deleteEntryComment s+          commit+          edComment . (! s) <$> getEntries+        comment `shouldBe` Nothing+  context "when pre-existing comment is overwritten" $+    it "returns the new comment" $ \path ->+      property $ \txt txt' s -> do+        comment <- createArchive path $ do+          addEntry Store "foo" s+          setEntryComment txt s+          commit+          setEntryComment txt' s+          commit+          edComment . (! s) <$> getEntries+        comment `shouldBe` Just txt'+  context "when pre-existing comment is deleted" $+    it "actually deletes it" $ \path ->+      property $ \txt s -> do+        comment <- createArchive path $ do+          addEntry Store "foo" s+          setEntryComment txt s+          commit+          deleteEntryComment s+          commit+          edComment . (! s) <$> getEntries+        comment `shouldBe` Nothing++setModTimeSpec :: SpecWith FilePath+setModTimeSpec = do+  context "when mod time is set (after creation)" $+    it "reads it and updates" $ \path ->+      property $ \time s -> do+        modTime <- createArchive path $ do+          addEntry Store "foo" s+          setModTime time s+          commit+          edModTime . (! s) <$> getEntries+        modTime `shouldSatisfy` isCloseTo time+  context "when mod time is set (before creation)" $+    it "has no effect" $ \path ->+      property $ \time time' s ->+        not (isCloseTo time time') ==> do+          modTime <- createArchive path $ do+            setModTime time s+            addEntry Store "foo" s+            commit+            edModTime . (! s) <$> getEntries+          modTime `shouldNotSatisfy` isCloseTo time++extraFieldSpec :: SpecWith FilePath+extraFieldSpec = do+  context "when extra field is committed (delete/set)" $+    it "reads it and updates" $ \path ->+      property $ \n b s ->+        n /= 1 ==> do+          efield <- createArchive path $ do+            addEntry Store "foo" s+            deleteExtraField n s+            addExtraField n b s+            commit+            M.lookup n . edExtraField . (! s) <$> getEntries+          efield `shouldBe` Just b+  context "when extra field is committed (set/delete)" $+    it "reads it and updates" $ \path ->+      property $ \n b s ->+        n /= 1 ==> do+          efield <- createArchive path $ do+            addEntry Store "foo" s+            addExtraField n b s+            deleteExtraField n s+            commit+            M.lookup n . edExtraField . (! s) <$> getEntries+          efield `shouldBe` Nothing+  context "when pre-existing extra field is overwritten" $+    it "reads it and updates" $ \path ->+      property $ \n b b' s ->+        n /= 1 ==> do+          efield <- createArchive path $ do+            addEntry Store "foo" s+            addExtraField n b s+            commit+            addExtraField n b' s+            commit+            M.lookup n . edExtraField . (! s) <$> getEntries+          efield `shouldBe` Just b'+  context "when pre-existing extra field is deleted" $+    it "actually deletes it" $ \path ->+      property $ \n b s ->+        n /= 1 ==> do+          efield <- createArchive path $ do+            addEntry Store "foo" s+            addExtraField n b s+            commit+            deleteExtraField n s+            commit+            M.lookup n . edExtraField . (! s) <$> getEntries+          efield `shouldBe` Nothing++setExternalFileAttrsSpec :: SpecWith FilePath+setExternalFileAttrsSpec =+  context "when an external file attribute is added (after creation)" $+    it "sets a custom external file attribute" $ \path ->+      property $ \attr s -> do+        attr' <- createArchive path $ do+          addEntry Store "foo" s+          setExternalFileAttrs attr s+          commit+          edExternalFileAttrs . (! s) <$> getEntries+        attr' `shouldBe` attr++renameEntrySpec :: SpecWith FilePath+renameEntrySpec = do+  context "when renaming after editing of new entry" $+    it "produces correct result" $ \path ->+      property $ \(EM s desc z) s' -> do+        desc' <- createArchive path $ do+          z+          renameEntry s s'+          commit+          (! s') <$> getEntries+        desc' `shouldSatisfy` softEq desc+  context "when renaming existing entry" $+    it "gets renamed" $ \path ->+      property $ \(EM s desc z) s' -> do+        desc' <- createArchive path $ do+          z+          commit+          renameEntry s s'+          commit+          (! s') <$> getEntries+        desc' `shouldSatisfy` softEq desc++deleteEntrySpec :: SpecWith FilePath+deleteEntrySpec = do+  context "when deleting after editing of new entry" $+    it "produces correct result" $ \path ->+      property $ \(EM s _ z) -> do+        member <- createArchive path $ do+          z+          deleteEntry s+          commit+          doesEntryExist s+        member `shouldBe` False+  context "when deleting existing entry" $+    it "gets deleted" $ \path ->+      property $ \(EM s _ z) -> do+        member <- createArchive path $ do+          z+          commit+          deleteEntry s+          commit+          doesEntryExist s+        member `shouldBe` False++forEntriesSpec :: SpecWith FilePath+forEntriesSpec =+  it "affects all existing entries" $ \path -> property $ \(EC m z) txt -> do+    m' <- createArchive path $ do+      z+      commit+      forEntries (setEntryComment txt)+      commit+      getEntries+    let f ed = ed {edComment = Just txt}+    m' `shouldSatisfy` softEqMap (M.map f m)++undoEntryChangesSpec :: SpecWith FilePath+undoEntryChangesSpec =+  it "cancels all actions for specified entry" $+    \path -> property $ \(EM s _ z) -> do+      member <- createArchive path $ do+        z+        undoEntryChanges s+        commit+        doesEntryExist s+      member `shouldBe` False++undoArchiveChangesSpec :: SpecWith FilePath+undoArchiveChangesSpec = do+  it "cancels archive comment editing" $ \path -> property $ \txt -> do+    comment <- createArchive path $ do+      setArchiveComment txt+      undoArchiveChanges+      commit+      getArchiveComment+    comment `shouldBe` Nothing+  it "cancels archive comment deletion" $ \path -> property $ \txt -> do+    comment <- createArchive path $ do+      setArchiveComment txt+      commit+      deleteArchiveComment+      undoArchiveChanges+      commit+      getArchiveComment+    comment `shouldBe` Just txt++undoAllSpec :: SpecWith FilePath+undoAllSpec =+  it "cancels all editing at once" $ \path -> property $ \(EC _ z) txt -> do+    createArchive path (return ())+    withArchive path $ do+      z+      setArchiveComment txt+      undoAll+      liftIO (B.writeFile path B.empty)+    B.readFile path `shouldReturn` B.empty++----------------------------------------------------------------------------+-- Complex construction/restoration++consistencySpec :: SpecWith FilePath+consistencySpec =+  it "can save and restore arbitrary archive" $ \path ->+    property $ \(EC m z) txt -> do+      (txt', m') <- createArchive path $ do+        z+        setArchiveComment txt+        commit+        (,) <$> getArchiveComment <*> getEntries+      txt' `shouldBe` Just txt+      m' `shouldSatisfy` softEqMap m++packDirRecur'Spec :: SpecWith FilePath+packDirRecur'Spec =+  it "packs arbitrary directory recursively" $ \path ->+    property $+      forAll (downScale arbitrary) $ \contents ->+        withSystemTempDirectory "zip-sandbox" $ \dir -> do+          forM_ contents $ \s -> do+            let item = dir </> unEntrySelector s+            createDirectoryIfMissing True (FP.takeDirectory item)+            B.writeFile item "foo"+          let magicFileAttrs = 123456789+          entries <-+            createArchive path $ do+              packDirRecur'+                Store+                mkEntrySelector+                (setExternalFileAttrs magicFileAttrs)+                dir+              commit+              getEntries+          M.keysSet entries `shouldBe` E.fromList contents+          forM_ (M.elems entries) $ \desc ->+            edExternalFileAttrs desc `shouldBe` magicFileAttrs++unpackIntoSpec :: SpecWith FilePath+unpackIntoSpec =+  it "unpacks archive contents into directory" $ \path ->+    property $ \(EC m z) ->+      withSystemTempDirectory "zip-sandbox" $ \dir -> do+        createArchive path $ do+          z+          commit+          unpackInto dir+        selectors <- listDirRecur dir >>= mapM mkEntrySelector+        let x = E.fromList selectors+            y = M.keysSet m+        E.difference x y `shouldBe` E.empty++----------------------------------------------------------------------------+-- Helpers++-- | Change the size parameter of a generator by dividing it by 2.+downScale :: Gen a -> Gen a+downScale = scale (`div` 2)++-- | Check whether a given exception is 'EntrySelectorException' with a+-- specific path inside.+isEntrySelectorException :: FilePath -> EntrySelectorException -> Bool+isEntrySelectorException path (InvalidEntrySelector p) = p == path++-- | Check whether a given exception is 'ParsingFailed' exception with a+-- specific path and error message inside.+isParsingFailed :: FilePath -> String -> ZipException -> Bool+isParsingFailed path msg (ParsingFailed path' msg') =+  path == path' && msg == msg'+isParsingFailed _ _ _ = False++-- | Create a sandbox directory to model some situation in it and run some+-- tests. Note that we're using a new unique sandbox directory for each test+-- case to avoid contamination and it's unconditionally deleted after the+-- test case finishes. The function returns a vacant file path in that+-- directory.+withSandbox :: ActionWith FilePath -> IO ()+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+deriveVacant = (</> "bar") . FP.takeDirectory++-- | Compare times forgiving a minor difference.+isCloseTo :: UTCTime -> UTCTime -> Bool+isCloseTo a b = abs (diffUTCTime a b) < 2++-- | Compare for equality taking into account only some fields of the+-- 'EntryDescription' record.+softEq :: EntryDescription -> EntryDescription -> Bool+softEq a b =+  edCompression a == edCompression b+    && isCloseTo (edModTime a) (edModTime b)+    && edUncompressedSize a == edUncompressedSize b+    && edComment a == edComment b+    && M.delete 1 (edExtraField a) == M.delete 1 (edExtraField b)++-- | Compare two maps describing archive entries in such a way that only+-- some fields in 'EntryDescription' record are tested.+softEqMap ::+  Map EntrySelector EntryDescription ->+  Map EntrySelector EntryDescription ->+  Bool+softEqMap n m = M.null (M.differenceWith f n m)+  where+    f a b = if softEq a b then Nothing else Just a++-- | The canonical representation of an empty Zip archive.+emptyArchive :: ByteString+emptyArchive =+  B.pack+    [ 0x50,+      0x4b,+      0x05,+      0x06,+      0x00,+      0x00,+      0x00,+      0x00,+      0x00,+      0x00,+      0x00,+      0x00,+      0x00,+      0x00,+      0x00,+      0x00,+      0x00,+      0x00,+      0x00,+      0x00,+      0x00,+      0x00+    ]++-- | Recursively list a directory. Do not return paths to empty directories.+listDirRecur :: FilePath -> IO [FilePath]+listDirRecur path = DList.toList <$> go ""+  where+    go adir = do+      let cdir = path </> adir+      raw <- listDirectory cdir+      fmap mconcat . forM raw $ \case+        "" -> return mempty+        "." -> return mempty+        ".." -> return mempty+        x -> do+          let fullx = cdir </> x+              adir' = adir </> x+          isFile <- doesFileExist fullx+          isDir <- doesDirectoryExist fullx+          if isFile+            then return (DList.singleton adir')+            else+              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,83 +1,150 @@-name:                 zip-version:              0.1.11-cabal-version:        >= 1.10-tested-with:          GHC==7.10.3, GHC==8.0.2, GHC==8.2.1-license:              BSD3-license-file:         LICENSE.md-author:               Mark Karpov <markkarpov92@gmail.com>-maintainer:           Mark Karpov <markkarpov92@gmail.com>-homepage:             https://github.com/mrkkrp/zip-bug-reports:          https://github.com/mrkkrp/zip/issues-category:             Codec-synopsis:             Operations on zip archives-build-type:           Simple-description:          Operations on zip archives.-extra-doc-files:      CHANGELOG.md-                    , README.md+cabal-version:   2.4+name:            zip+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 ==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+description:     Operations on zip archives.+category:        Codec+build-type:      Simple+extra-doc-files:+    CHANGELOG.md+    README.md +source-repository head+    type:     git+    location: https://github.com/mrkkrp/zip.git+ flag dev-  description:        Turn on development settings.-  manual:             True-  default:            False+    description: Turn on development settings.+    default:     False+    manual:      True +flag disable-bzip2+    description:+        Removes dependency on bzip2 C library and hence support for BZip2 entries.++    default:     False+    manual:      True++flag disable-zstd+    description:+        Removes dependency on zstd C library and hence support for Zstandard entries.++    default:     False+    manual:      True+ library-  build-depends:      base             >= 4.8     && < 5.0-                    , bytestring       >= 0.9     && < 0.11-                    , bzlib-conduit    >= 0.2     && < 0.3-                    , case-insensitive >= 1.2.0.2 && < 1.3-                    , cereal           >= 0.3     && < 0.6-                    , conduit          >= 1.1     && < 2.0-                    , conduit-extra    >= 1.1     && < 2.0-                    , containers       >= 0.5.6.2 && < 0.6-                    , digest           < 0.1-                    , exceptions       >= 0.6     && < 0.9-                    , filepath         >= 1.2     && < 1.5-                    , mtl              >= 2.0     && < 3.0-                    , path             >= 0.5     && < 0.6-                    , path-io          >= 1.0.1   && < 2.0-                    , plan-b           >= 0.2     && < 0.3-                    , resourcet        >= 1.0     && < 2.0-                    , text             >= 0.2     && < 1.3-                    , time             >= 1.4     && < 1.9-                    , transformers     >= 0.4     && < 0.6-  if !impl(ghc >= 8.0)-    build-depends:    semigroups       >= 0.16-  default-extensions: RecordWildCards-                    , TupleSections-  exposed-modules:    Codec.Archive.Zip-                    , Codec.Archive.Zip.CP437-                    , Codec.Archive.Zip.Type-  other-modules:      Codec.Archive.Zip.Internal-  if flag(dev)-    ghc-options:      -Wall -Werror-  else-    ghc-options:      -O2 -Wall-  default-language:   Haskell2010+    exposed-modules:+        Codec.Archive.Zip+        Codec.Archive.Zip.CP437+        Codec.Archive.Zip.Unix+        Codec.Archive.Zip.Internal+        Codec.Archive.Zip.Internal.Type +    default-language: GHC2021+    build-depends:+        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.9,+        digest <0.1,+        directory >=1.2.2 && <1.4,+        dlist >=0.8 && <2.0,+        exceptions >=0.6 && <0.11,+        filepath >=1.2 && <1.6,+        monad-control >=1.0 && <1.1,+        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)+        build-depends: bzlib-conduit >=0.3 && <0.4++    if !flag(disable-zstd)+        build-depends: conduit-zstd >=0.0.2 && <0.1++    if flag(dev)+        cpp-options: -DHASKELL_ZIP_DEV_MODE+        ghc-options:+            -Wall -Werror -Wredundant-constraints -Wpartial-fields+            -Wunused-packages++    else+        ghc-options: -O2 -Wall++    if !flag(disable-bzip2)+        cpp-options: -DENABLE_BZIP2++    if !flag(disable-zstd)+        cpp-options: -DENABLE_ZSTD++    if os(windows)+        cpp-options: -DZIP_OS=0++    else+        cpp-options:   -DZIP_OS=3+        build-depends: unix <2.9++executable haskell-zip-app+    main-is:          Main.hs+    hs-source-dirs:   bench-app+    default-language: GHC2021+    build-depends:+        base >=4.15 && <5,+        filepath >=1.2 && <1.6,+        zip++    if flag(dev)+        ghc-options:+            -Wall -Werror -Wredundant-constraints -Wpartial-fields+            -Wunused-packages++    else+        ghc-options: -O2 -Wall+ test-suite tests-  main-is:            Main.hs-  hs-source-dirs:     tests-  type:               exitcode-stdio-1.0-  if flag(dev)-    ghc-options:      -Wall -Werror-  else-    ghc-options:      -O2 -Wall-  build-depends:      base             >= 4.8     && < 5.0-                    , bytestring       >= 0.9     && < 0.11-                    , conduit          >= 1.1     && < 2.0-                    , containers       >= 0.5.6.2 && < 0.6-                    , exceptions       >= 0.6     && < 0.9-                    , filepath         >= 1.2     && < 1.5-                    , QuickCheck       >= 2.4     && < 3.0-                    , hspec            >= 2.0     && < 3.0-                    , path             >= 0.5     && < 6.0-                    , path-io          >= 1.0.1   && < 2.0-                    , text             >= 0.2     && < 1.3-                    , time             >= 1.4     && < 1.9-                    , transformers     >= 0.4     && < 0.6-                    , zip-  default-language:   Haskell2010+    type:             exitcode-stdio-1.0+    main-is:          Main.hs+    hs-source-dirs:   tests+    default-language: GHC2021+    build-depends:+        base >=4.15 && <5,+        QuickCheck >=2.4 && <3,+        bytestring >=0.9 && <0.13,+        conduit >=1.3 && <1.4,+        containers >=0.5 && <0.9,+        directory >=1.2.2 && <1.4,+        dlist >=0.8 && <2,+        filepath >=1.2 && <1.6,+        hspec >=2 && <3,+        temporary >=1.1 && <1.4,+        text >=0.2 && <2.2,+        time >=1.4 && <1.16,+        zip -source-repository head-  type:               git-  location:           https://github.com/mrkkrp/zip.git+    if flag(dev)+        cpp-options: -DHASKELL_ZIP_DEV_MODE+        ghc-options:+            -Wall -Werror -Wredundant-constraints -Wpartial-fields+            -Wunused-packages++    else+        ghc-options: -O2 -Wall++    if !flag(disable-bzip2)+        cpp-options: -DENABLE_BZIP2++    if !flag(disable-zstd)+        cpp-options: -DENABLE_ZSTD