diff --git a/Main.hs b/Main.hs
new file mode 100644
--- /dev/null
+++ b/Main.hs
@@ -0,0 +1,100 @@
+------------------------------------------------------------------------
+-- Zip.hs
+-- Copyright (c) 2008 John MacFarlane
+-- License     : BSD3 (see LICENSE)
+--
+-- This is a demonstration of the use of the 'Codec.Archive.Zip' library.
+-- It duplicates some of the functionality of the 'zip' command-line
+-- program.
+------------------------------------------------------------------------
+
+import Codec.Archive.Zip
+import System.IO
+import qualified Data.ByteString.Lazy as B
+import System.Exit
+import System.Environment
+import System.Directory
+import System.Console.GetOpt
+import Control.Monad ( when )
+import Data.Version ( showVersion )
+import Paths_zip_archive ( version )
+import Debug.Trace ( traceShowId )
+
+data Flag
+  = Quiet
+  | Version
+  | Decompress
+  | Recursive
+  | Remove
+  | List
+  | Debug
+  | Help
+  deriving (Eq, Show, Read)
+
+options :: [OptDescr Flag]
+options =
+   [ Option ['d']   ["decompress"] (NoArg Decompress)    "decompress (unzip)"
+   , Option ['r']   ["recursive"]  (NoArg Recursive)     "recursive"
+   , Option ['R']   ["remove"]     (NoArg Remove)        "remove"
+   , Option ['l']   ["list"]       (NoArg List)          "list"
+   , Option ['v']   ["version"]    (NoArg Version)       "version"
+   , Option ['q']   ["quiet"]      (NoArg Quiet)         "quiet"
+   , Option []      ["debug"]      (NoArg Debug)         "debug output"
+   , Option ['h']   ["help"]       (NoArg Help)          "help"
+   ]
+
+quit :: Bool -> String -> IO a
+quit failure msg = do
+  hPutStr stderr msg
+  _ <- exitWith $ if failure
+                     then ExitFailure 1
+                     else ExitSuccess
+  return undefined
+
+main :: IO ()
+main = do
+  argv <- getArgs
+  progname <- getProgName
+  let header = "Usage: " ++ progname ++ " [OPTION...] archive files..."
+  (opts, args) <- case getOpt Permute options argv of
+      (o, _, _)      | Version `elem` o -> do
+        putStrLn ("version " ++ showVersion version)
+        exitWith ExitSuccess
+      (o, _, _)      | Help `elem` o    -> quit False $ usageInfo header options
+      (o, (a:as), [])                   -> return (o, a:as)
+      (_, [], [])                       -> quit True $ usageInfo header options
+      (_, _, errs)                      -> quit True $ concat errs ++ "\n" ++ usageInfo header options
+  let verbosity = if Quiet `elem` opts then [] else [OptVerbose]
+  let debug = Debug `elem` opts
+  let cmd = case filter (`notElem` [Quiet, Help, Version, Debug]) opts of
+                  []    -> Recursive
+                  (x:_) -> x
+  (archivePath : files) <- case args of
+      [] -> quit True "No archive path given"
+      _ -> return args
+  exists <- doesFileExist archivePath
+  archive <- if exists
+                then toArchive <$> B.readFile archivePath
+                else return emptyArchive
+  let showArchiveIfDebug x = if debug
+                                then traceShowId x
+                                else x
+  case cmd of
+       Decompress  -> extractFilesFromArchive verbosity $ showArchiveIfDebug archive
+       Remove      -> do tempDir <- getTemporaryDirectory
+                         (tempArchivePath, tempArchive) <- openTempFile tempDir "zip"
+                         B.hPut tempArchive $ fromArchive $ showArchiveIfDebug $
+                                              foldr deleteEntryFromArchive archive files
+                         hClose tempArchive
+                         copyFile tempArchivePath archivePath
+                         removeFile tempArchivePath
+       List        -> mapM_ putStrLn $ filesInArchive $ showArchiveIfDebug archive
+       Recursive   -> do when (null files) $ error "No files specified."
+                         tempDir <- getTemporaryDirectory
+                         (tempArchivePath, tempArchive) <- openTempFile tempDir "zip"
+                         addFilesToArchive (verbosity ++ [OptRecursive]) archive files >>=
+                            B.hPut tempArchive . fromArchive . showArchiveIfDebug
+                         hClose tempArchive
+                         copyFile tempArchivePath archivePath
+                         removeFile tempArchivePath
+       _           -> error $ "Unknown command " ++ show cmd
diff --git a/README.markdown b/README.markdown
--- a/README.markdown
+++ b/README.markdown
@@ -1,6 +1,27 @@
 zip-archive
 ===========
 
-The zip-archive library provides functions for creating, modifying, and
-extracting files from zip archives.
+The zip-archive library provides functions for creating, modifying,
+and extracting files from zip archives.  The zip archive format
+is documented in
+<http://www.pkware.com/documents/casestudies/APPNOTE.TXT>.
 
+Certain simplifying assumptions are made about the zip archives:
+in particular, there is no support for strong encryption, zip
+files that span multiple disks, ZIP64, OS-specific file
+attributes, or compression methods other than Deflate.  However,
+the library should be able to read the most common zip archives,
+and the archives it produces should be readable by all standard
+unzip programs.
+
+Archives are built and extracted in memory, so manipulating
+large zip files will consume a lot of memory.  If you work with
+large zip files or need features not supported by this library,
+a better choice may be [zip](http://hackage.haskell.org/package/zip),
+which uses a memory-efficient streaming approach.  However, zip
+can only read and write archives inside instances of MonadIO, so
+zip-archive is a better choice if you want to manipulate zip
+archives in "pure" contexts.
+
+As an example of the use of the library, a standalone zip archiver
+and extracter is provided in the source distribution.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/Setup.lhs b/Setup.lhs
deleted file mode 100644
--- a/Setup.lhs
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/usr/bin/env runhaskell
-
-> module Main ( main ) where
->
-> import Distribution.Simple
-> import Distribution.Simple.Program
->
-> main :: IO ()
-> main = defaultMainWithHooks simpleUserHooks
->        { hookedPrograms = [ simpleProgram "zip" ]
->        }
diff --git a/Zip.hs b/Zip.hs
deleted file mode 100644
--- a/Zip.hs
+++ /dev/null
@@ -1,80 +0,0 @@
-------------------------------------------------------------------------
--- Zip.hs
--- Copyright (c) 2008 John MacFarlane
--- License     : BSD3 (see LICENSE)
---
--- This is a demonstration of the use of the 'Codec.Archive.Zip' library.
--- It duplicates some of the functionality of the 'zip' command-line
--- program.
-------------------------------------------------------------------------
-
-import Codec.Archive.Zip
-import System.IO
-import qualified Data.ByteString.Lazy as B
-import System.Exit
-import System.Environment
-import System.Directory
-import System.Console.GetOpt
-import Control.Monad ( when )
-import Control.Applicative ( (<$>) )
-
-data Flag 
-  = Quiet 
-  | Version
-  | Decompress
-  | Recursive
-  | Remove
-  | List
-  | Help
-  deriving (Eq, Show, Read)
-
-options :: [OptDescr Flag]
-options =
-   [ Option ['d']   ["decompress"] (NoArg Decompress)    "decompress (unzip)"
-   , Option ['r']   ["recursive"]  (NoArg Recursive)     "recursive"
-   , Option ['R']   ["remove"]     (NoArg Remove)        "remove"
-   , Option ['l']   ["list"]       (NoArg List)          "list"
-   , Option ['v']   ["version"]    (NoArg Version)       "version"
-   , Option ['q']   ["quiet"]      (NoArg Quiet)         "quiet"
-   , Option ['h']   ["help"]       (NoArg Help)          "help"
-   ]
-
-main :: IO ()
-main = do
-  argv <- getArgs
-  progname <- getProgName
-  let header = "Usage: " ++ progname ++ " [OPTION...] archive files..."
-  (opts, args) <- case getOpt Permute options argv of
-      (o, _, _)      | Version `elem` o -> putStrLn "version 0.1.1.4" >> exitWith ExitSuccess
-      (o, _, _)      | Help `elem` o    -> error $ usageInfo header options
-      (o, (a:as), [])                   -> return (o, a:as)
-      (_, _, errs)                      -> error $ concat errs ++ "\n" ++ usageInfo header options
-  let verbosity = if Quiet `elem` opts then [] else [OptVerbose]
-  let cmd = take 1 $ filter (`notElem` [Quiet, Help, Version]) opts
-  let cmd' = if null cmd
-                then Recursive
-                else head cmd
-  let (archivePath : files) = args
-  exists <- doesFileExist archivePath
-  archive <- if exists
-                then toArchive <$> B.readFile archivePath
-                else return emptyArchive
-  case cmd' of
-       Decompress  -> extractFilesFromArchive verbosity archive  
-       Remove      -> do tempDir <- getTemporaryDirectory
-                         (tempArchivePath, tempArchive) <- openTempFile tempDir "zip" 
-                         B.hPut tempArchive $ fromArchive $ 
-                                              foldr deleteEntryFromArchive archive files
-                         hClose tempArchive
-                         copyFile tempArchivePath archivePath
-                         removeFile tempArchivePath
-       List        -> mapM_ putStrLn $ filesInArchive archive
-       Recursive   -> do when (null files) $ error "No files specified."
-                         tempDir <- getTemporaryDirectory
-                         (tempArchivePath, tempArchive) <- openTempFile tempDir "zip" 
-                         addFilesToArchive (verbosity ++ [OptRecursive]) archive files >>= 
-                            B.hPut tempArchive . fromArchive
-                         hClose tempArchive
-                         copyFile tempArchivePath archivePath
-                         removeFile tempArchivePath
-       _           -> error "Unknown command"
diff --git a/changelog b/changelog
--- a/changelog
+++ b/changelog
@@ -1,3 +1,224 @@
+zip-archive 0.4.3.2
+
+  * readEntry: Fix computation of modification time (#67).
+    It should be a UNIX time (seconds since UNIX epoch), but
+    computed relative to the local time zone, not UTC.
+
+zip-archive 0.4.3.1
+
+  * Use streaming decompress to identify extent of compressed data (#66).
+    This fixes a problem that arises for local files with bit 3
+    of the general purpose bit flag set. In this case, we don't
+    get information up front about the size of the compressed
+    data.  So how do we know where the compressed data ends?
+    Previously, we tried to determine this by looking for the
+    signature of the data descriptor. But the data descriptor doesn't
+    always HAVE a signature, and it is also possible for signatures to
+    occur accidentally in the compressed data itself (#65).
+    Instead, we now use the streaming decompression interface from
+    zlib's Internal module to identify where the compressed data
+    ends. Fixes both #65 and #25.
+
+    zip-archive 0.4.3
+
+  * Improve code for retrieving compressed data of unknown length (#63).
+    Do not assume we'll have the signature 0x08074b50 that is
+    sometimes used for the data description, because it is not
+    in the spec and is not always used.
+  * Make some record fields strict.
+  * Require binary >= 0.7.2, remove some CPP
+
+zip-archive 0.4.2.2
+
+  * Use `command -v` before trying `which` in the test suite (#62).
+    `command` is a bash builtin, but for busybox we'll need `which`.
+
+zip-archive 0.4.2.1
+
+  * Fix Windows build regression (#61).
+
+zip-archive 0.4.2
+
+  * Fix problem with files with colon (#89).
+  * Remove build-tools.  This was used to indicate that the 'unzip'
+    executable was needed for testing, but it was never intended to be used
+    this way and now the field is deprecated.  The current test suite
+    simply skips the test using the unzip executable (with a warning) if
+    'unzip' is not in the path.
+  * Remove existing symlinks when extracting zip files with symlinks (#60,
+    Vikrem).  Previously, writeEntry would raise an error if it tried to
+    create a symlink and a symlink already existed at that path.  This
+    behavior was inconsistent with its behavior for regular files, which
+    it overwrote without comment.  This commit causes symlinks to be replaced
+    by writeEntry instead of an error being raised.
+  * Remove binary < 0.6 CPP.  It's no longer needed because we don't support
+    binary < 0.6.  Also use manySig instead of many, to get better error
+    messages.
+  * Add type annotation for printf.
+  * Better checking for unsafe paths (#55).  This method allows things like
+    `foo/bar/../../baz`.
+  * Require base >= 4.5 (#56)
+  * Add GitHub CI.
+
+zip-archive 0.4.1
+
+  * writEntry behavior change: Improve raising of UnsafePath error (#55).
+    Previously we raised this error spuriously when archives were unpacked
+    outside the working directory.  Now we raise it if eRelativePath contains
+    ".." as a path component, or eRelativePath path is an absolute path and
+    there is no separate destination directory.  (Note that `/foo/bar` is fine
+    as a path as long as a destination directory, e.g. `/usr/local`, is
+    specified.)
+
+zip-archive 0.4
+
+  * Implement read-only support for PKWARE encryption (Sergii Rudchenko).
+    The "traditional" PKWARE encryption is a symmetric encryption
+    algorithm described in zip format specification in section 6.1.
+    This change allows to extract basic "password-protected" entries from
+    ZIP files.  Note that the standard file extraction function
+    extractFilesFromArchive does not decrypt entries (it will raise
+    an exception if it encounters an encrypted entry). To handle
+    archives with encrypted entries, use the new function
+    fromEncryptedEntry.
+
+    API changes:
+
+    + Add eEncryptionMethod field to Entry.
+    + Add EncryptionMethod type.
+    + Add function isEncryptedEntry.
+    + Add function fromEncryptedEntry.
+    * Add CannotWriteEncryptedEntry constructor to ZipException.
+
+  * Add UnsafePath to ZipException (#50).
+  * writeEntry: raise UnsafePath exception for unsafe paths (#50).
+    This prevents malicious zip files from overwriting paths
+    above the working directory.
+  * Add Paths_zip_archive to autogen-modules.
+  * Clarify README and cabal description.
+  * Specify cabal-version: 2.0.  Otherwise we get an unknown build
+    tool error using `build-depends` without a custom Setup.hs.
+  * Change build-type to simple.  Retain 'build-tools: unzip' in
+    test stanza, though now it doesn't do anything except give a
+    hint to external tools.  If unzip is not found in the path,
+    the test suite prints a message and counts the test that
+    requires unzip as succeeding (see #51).
+
+zip-archive 0.3.3
+
+  * Remove dependency on old-time (typedrat).
+  * Drop splitBase flag and support for base versions < 3.
+
+zip-archive 0.3.2.5
+
+  * Move 'build-tools: unzip' from library stanza to test stanza.
+    unzip should only be required for testing, not for regular
+    builds of the library.
+
+zip-archive 0.3.2.4
+
+  * Make build-tools stanza conditional on non-windows. Closes #44.
+
+zip-archive 0.3.2.3
+
+  * Use custom-setup stanza and specify build-tools.  Closes #41.
+
+zip-archive 0.3.2.2
+
+  * Use createSymbolicLink instead of createFileLink in tests. This allows
+    us to lower the directory lower bound (#40).
+
+zip-archive 0.3.2.1
+
+  * Fixes for handling of symbolic links (#39, Tommaso Piazza).
+
+  * Fixes for symbolic link tests, and additional tests.
+
+zip-archive 0.3.2
+
+  * Add ZipOption to preserve symbolic links (#37, Tommaso Piazza).
+    Add OptPreserveSymbolicLinks constructor to ZipOption.  If this option
+    is set, symbolic links will be preserved.  Symbolic links are not
+    supported on Windows.
+
+  * Require binary >= 0.6 (#36).
+
+  * Improve exit handling in zip-archive program.
+
+zip-archive 0.3.1.1
+
+  * readEntry:  Read file as a strict ByteString.  This avoids
+    problems on Windows, where the file handle wasn't being closed.
+  * Added appveyor.yml to do continuous testing on Windows.
+  * Test suite: remove need for external zip program (#35).
+    Instead of creating an archive with zip, we now store
+    a small externally created zip archive to use for testing.
+
+zip-archive 0.3.1
+
+  * Don't use a custom build (#28).
+  * Renamed executable Zip -> zip-archive, added --debug option.
+    The --debug option prints the intermediate Haskell data structure.
+
+zip-archive 0.3.0.7
+
+  * Fix check for unix file attributes (#34).
+    Previously attributes would not always be preserved
+    for files in zip archives.
+
+zip-archive 0.3.0.6
+
+  * Bump bytestring lower bound so toStrict is guaranteed (Benjamin Landers).
+
+zip-archive 0.3.0.5
+
+  * Fix bug in `OptLocation` handling (EugeneN).  When using
+    `OptLocation folder False` (for adding files to an archive into a
+    folder without preserving full path hierarchy), original files'
+    names were ignored, resulting in all the files getting the same name.
+
+zip-archive 0.3.0.4
+
+  * Fix `toArchive` so it doesn't use too much memory when a data
+    data descriptor holds the size (Michael Stahl, #29).
+    The size fields in the local file headers may not contain valid values,
+    in which case the sizes are stored in a "data descriptor" that follows
+    the file data.  Previously handling this case required reading the
+    entire archive is a `[Word8]` list.  With this change, `getWordsTilSig`
+    iteratively reads chunks as strict ByteStrings and converts them to
+    a lazy ByteString at the end.
+
+zip-archive 0.3.0.3
+
+  * Test suite: use withTempDir to create temporary directory.
+    This should help fix problems some have encountered with the
+    test suite leaving a temporary directory behind.
+
+zip-archive 0.3.0.2
+
+  * Fix test suite so it runs on Windows.
+  * Zip executable: get version from cabal `Paths_zip_archive` (#27).
+
+zip-archive 0.3.0.1
+
+  * Set `eVersionMadeBy` to 0 (default) in `toEntry`, since we are
+    setting external attributes to 0.  See jgm/pandoc#2822.
+    Only to `eVersionMadeBy` to UNIX if we actually read file
+    attributes on a UNIX system.
+
+zip-archive 0.3
+
+  * Support preservation of file modes on Posix (Dan Aloni, #26).
+  * Add `eVersionMadeBy` field to `Entry` (API change).
+  * Export `ZipException` (API change).
+  * `fromEntry` no longer checks for CRC32 match.  Previously, it issued
+    `error` if the match failed.  CRC32 match is now checked in `writeEntry`
+    instead, and a `CRC32Exception` is raised if the checksum doesn't match.
+  * Test suite: return nonzero status if there are test failures.
+    Previously we mistakenly did this only on 'errors', not failures.
+  * Test suite: don't use -9 with zip as it isn't always available.
+  * Use .travis.yml that builds on both stack and cabal.
+
 zip-archive 0.2.3.7
 
   * Declared test suite's dependency on 'zip' using custom Setup.lhs (#21,#22).
diff --git a/src/Codec/Archive/Zip.hs b/src/Codec/Archive/Zip.hs
--- a/src/Codec/Archive/Zip.hs
+++ b/src/Codec/Archive/Zip.hs
@@ -1,4 +1,7 @@
 {-# LANGUAGE CPP #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE ViewPatterns #-}
 ------------------------------------------------------------------------
 -- |
 -- Module      : Codec.Archive.Zip
@@ -13,7 +16,7 @@
 -- and extracting files from zip archives.
 --
 -- Certain simplifying assumptions are made about the zip archives: in
--- particular, there is no support for encryption, zip files that span
+-- particular, there is no support for strong encryption, zip files that span
 -- multiple disks, ZIP64, OS-specific file attributes, or compression
 -- methods other than Deflate.  However, the library should be able to
 -- read the most common zip archives, and the archives it produces should
@@ -33,7 +36,9 @@
          Archive (..)
        , Entry (..)
        , CompressionMethod (..)
+       , EncryptionMethod (..)
        , ZipOption (..)
+       , ZipException (..)
        , emptyArchive
 
        -- * Pure functions for working with zip archives
@@ -45,42 +50,62 @@
        , deleteEntryFromArchive
        , findEntryByPath
        , fromEntry
+       , fromEncryptedEntry
+       , isEncryptedEntry
        , toEntry
+#ifndef _WINDOWS
+       , isEntrySymbolicLink
+       , symbolicLinkEntryTarget
+       , entryCMode
+#endif
 
        -- * IO functions for working with zip archives
        , readEntry
        , writeEntry
+#ifndef _WINDOWS
+       , writeSymbolicLinkEntry
+#endif
        , addFilesToArchive
        , extractFilesFromArchive
 
        ) where
 
-import System.Time ( toUTCTime, addToClockTime, CalendarTime (..), ClockTime (..), TimeDiff (..) )
-#if MIN_VERSION_directory(1,2,0)
-import Data.Time.Clock.POSIX ( utcTimeToPOSIXSeconds )
-#endif
-import Data.Bits ( shiftL, shiftR, (.&.) )
+import Data.Time.Calendar ( toGregorian, fromGregorian )
+import Data.Time.Clock ( UTCTime(..) )
+import Data.Time.LocalTime ( TimeZone(..), TimeOfDay(..), timeToTimeOfDay,
+                             getTimeZone )
+import Data.Time.Clock.POSIX ( posixSecondsToUTCTime, utcTimeToPOSIXSeconds )
+import Data.Bits ( shiftL, shiftR, (.&.), (.|.), xor, testBit )
 import Data.Binary
 import Data.Binary.Get
 import Data.Binary.Put
-import Data.List ( nub, find, intercalate )
+import Data.List (nub, find, intercalate)
+import Data.Data (Data)
+import Data.Typeable (Typeable)
 import Text.Printf
 import System.FilePath
-import System.Directory ( doesDirectoryExist, getDirectoryContents, createDirectoryIfMissing )
-import Control.Monad ( when, unless, zipWithM )
-import System.Directory ( getModificationTime )
+import System.Directory
+       (doesDirectoryExist, getDirectoryContents,
+        createDirectoryIfMissing, getModificationTime,)
+import Control.Monad ( when, unless, zipWithM_ )
+import qualified Control.Exception as E
 import System.IO ( stderr, hPutStrLn )
 import qualified Data.Digest.CRC32 as CRC32
 import qualified Data.Map as M
-#if MIN_VERSION_binary(0,6,0)
 import Control.Applicative
-#endif
-#ifndef _WINDOWS
-import System.Posix.Files ( setFileTimes )
+#ifdef _WINDOWS
+import Data.Char (isLetter)
+#else
+import System.Posix.Files ( setFileTimes, setFileMode, fileMode, getSymbolicLinkStatus, symbolicLinkMode, readSymbolicLink, isSymbolicLink, unionFileModes, createSymbolicLink, removeLink )
+import System.Posix.Types ( CMode(..) )
+import Data.List (partition)
+import Data.Maybe (fromJust)
 #endif
 
 -- from bytestring
+import qualified Data.ByteString as S
 import qualified Data.ByteString.Lazy as B
+import qualified Data.ByteString.Lazy.Char8 as C
 
 -- text
 import qualified Data.Text.Lazy as TL
@@ -88,8 +113,11 @@
 
 -- from zlib
 import qualified Codec.Compression.Zlib.Raw as Zlib
+import qualified Codec.Compression.Zlib.Internal as ZlibInt
+import System.IO.Error (isAlreadyExistsError)
 
-#if !MIN_VERSION_binary(0, 6, 0)
+-- import Debug.Trace
+
 manySig :: Word32 -> Get a -> Get [a]
 manySig sig p = do
     sig' <- lookAhead getWord32le
@@ -99,7 +127,6 @@
             rs <- manySig sig p
             return $ r : rs
         else return []
-#endif
 
 
 ------------------------------------------------------------------------
@@ -109,7 +136,7 @@
 data Archive = Archive
                 { zEntries                :: [Entry]              -- ^ Files in zip archive
                 , zSignature              :: Maybe B.ByteString   -- ^ Digital signature
-                , zComment                :: B.ByteString         -- ^ Comment for whole zip archive
+                , zComment                :: !B.ByteString        -- ^ Comment for whole zip archive
                 } deriving (Read, Show)
 
 instance Binary Archive where
@@ -119,16 +146,18 @@
 -- | Representation of an archived file, including content and metadata.
 data Entry = Entry
                { eRelativePath            :: FilePath            -- ^ Relative path, using '/' as separator
-               , eCompressionMethod       :: CompressionMethod   -- ^ Compression method
-               , eLastModified            :: Integer             -- ^ Modification time (seconds since unix epoch)
-               , eCRC32                   :: Word32              -- ^ CRC32 checksum
-               , eCompressedSize          :: Word32              -- ^ Compressed size in bytes
-               , eUncompressedSize        :: Word32              -- ^ Uncompressed size in bytes
-               , eExtraField              :: B.ByteString        -- ^ Extra field - unused by this library
-               , eFileComment             :: B.ByteString        -- ^ File comment - unused by this library
-               , eInternalFileAttributes  :: Word16              -- ^ Internal file attributes - unused by this library
-               , eExternalFileAttributes  :: Word32              -- ^ External file attributes (system-dependent)
-               , eCompressedData          :: B.ByteString        -- ^ Compressed contents of file
+               , eCompressionMethod       :: !CompressionMethod   -- ^ Compression method
+               , eEncryptionMethod        :: !EncryptionMethod    -- ^ Encryption method
+               , eLastModified            :: !Integer             -- ^ Modification time (seconds since unix epoch)
+               , eCRC32                   :: !Word32              -- ^ CRC32 checksum
+               , eCompressedSize          :: !Word32              -- ^ Compressed size in bytes
+               , eUncompressedSize        :: !Word32              -- ^ Uncompressed size in bytes
+               , eExtraField              :: !B.ByteString        -- ^ Extra field - unused by this library
+               , eFileComment             :: !B.ByteString        -- ^ File comment - unused by this library
+               , eVersionMadeBy           :: !Word16              -- ^ Version made by field
+               , eInternalFileAttributes  :: !Word16              -- ^ Internal file attributes - unused by this library
+               , eExternalFileAttributes  :: !Word32              -- ^ External file attributes (system-dependent)
+               , eCompressedData          :: !B.ByteString        -- ^ Compressed contents of file
                } deriving (Read, Show, Eq)
 
 -- | Compression methods.
@@ -136,13 +165,31 @@
                        | NoCompression
                        deriving (Read, Show, Eq)
 
+data EncryptionMethod = NoEncryption             -- ^ Entry is not encrypted
+                      | PKWAREEncryption !Word8  -- ^ Entry is encrypted with the traditional PKWARE encryption
+                      deriving (Read, Show, Eq)
+
+-- | The way the password should be verified during entry decryption
+data PKWAREVerificationType = CheckTimeByte
+                            | CheckCRCByte
+                            deriving (Read, Show, Eq)
+
 -- | Options for 'addFilesToArchive' and 'extractFilesFromArchive'.
 data ZipOption = OptRecursive               -- ^ Recurse into directories when adding files
                | OptVerbose                 -- ^ Print information to stderr
                | OptDestination FilePath    -- ^ Directory in which to extract
-               | OptLocation FilePath Bool  -- ^ Where to place file when adding files and whether to append current path
+               | OptLocation FilePath !Bool -- ^ Where to place file when adding files and whether to append current path
+               | OptPreserveSymbolicLinks   -- ^ Preserve symbolic links as such. This option is ignored on Windows.
                deriving (Read, Show, Eq)
 
+data ZipException =
+    CRC32Mismatch FilePath
+  | UnsafePath FilePath
+  | CannotWriteEncryptedEntry FilePath
+  deriving (Show, Typeable, Data, Eq)
+
+instance E.Exception ZipException
+
 -- | A zip archive with no contents.
 emptyArchive :: Archive
 emptyArchive = Archive
@@ -160,13 +207,9 @@
 -- With earlier versions, it will always return a Right value,
 -- raising an error if parsing fails.
 toArchiveOrFail :: B.ByteString -> Either String Archive
-#if MIN_VERSION_binary(0,7,0)
 toArchiveOrFail bs = case decodeOrFail bs of
                            Left (_,_,e)  -> Left e
                            Right (_,_,x) -> Right x
-#else
-toArchiveOrFail bs = Right $ toArchive bs
-#endif
 
 -- | Writes an 'Archive' structure to a raw zip archive (in a lazy bytestring).
 fromArchive :: Archive -> B.ByteString
@@ -174,7 +217,7 @@
 
 -- | Returns a list of files in a zip archive.
 filesInArchive :: Archive -> [FilePath]
-filesInArchive = (map eRelativePath) . zEntries
+filesInArchive = map eRelativePath . zEntries
 
 -- | Adds an entry to a zip archive, or updates an existing entry.
 addEntryToArchive :: Entry -> Archive -> Archive
@@ -197,11 +240,20 @@
 -- | Returns uncompressed contents of zip entry.
 fromEntry :: Entry -> B.ByteString
 fromEntry entry =
-  let uncompressedData = decompressData (eCompressionMethod entry) (eCompressedData entry)
-  in  if eCRC32 entry == CRC32.crc32 uncompressedData
-         then uncompressedData
-         else error "CRC32 mismatch"
+  decompressData (eCompressionMethod entry) (eCompressedData entry)
 
+-- | Returns decrypted and uncompressed contents of zip entry.
+fromEncryptedEntry :: String -> Entry -> Maybe B.ByteString
+fromEncryptedEntry password entry =
+  decompressData (eCompressionMethod entry) <$> decryptData password (eEncryptionMethod entry) (eCompressedData entry)
+
+-- | Check if an 'Entry' is encrypted
+isEncryptedEntry :: Entry -> Bool
+isEncryptedEntry entry =
+  case eEncryptionMethod entry of
+    (PKWAREEncryption _) -> True
+    _ -> False
+
 -- | Create an 'Entry' with specified file path, modification time, and contents.
 toEntry :: FilePath         -- ^ File path for entry
         -> Integer          -- ^ Modification time for entry (seconds since unix epoch)
@@ -219,12 +271,14 @@
       crc32 = CRC32.crc32 contents
   in  Entry { eRelativePath            = normalizePath path
             , eCompressionMethod       = compressionMethod
+            , eEncryptionMethod        = NoEncryption
             , eLastModified            = modtime
             , eCRC32                   = crc32
             , eCompressedSize          = fromIntegral finalSize
             , eUncompressedSize        = fromIntegral uncompressedSize
             , eExtraField              = B.empty
             , eFileComment             = B.empty
+            , eVersionMadeBy           = 0  -- FAT
             , eInternalFileAttributes  = 0  -- potentially non-text
             , eExternalFileAttributes  = 0  -- appropriate if from stdin
             , eCompressedData          = finalData
@@ -234,39 +288,96 @@
 readEntry :: [ZipOption] -> FilePath -> IO Entry
 readEntry opts path = do
   isDir <- doesDirectoryExist path
-  -- make sure directories end in / and deal with the OptLocation option
+#ifdef _WINDOWS
+  let isSymLink = False
+#else
+  fs <- getSymbolicLinkStatus path
+  let isSymLink = isSymbolicLink fs
+#endif
+ -- make sure directories end in / and deal with the OptLocation option
   let path' = let p = path ++ (case reverse path of
                                     ('/':_) -> ""
-                                    _ | isDir -> "/"
+                                    _ | isDir && not isSymLink -> "/"
+                                    _ | isDir && isSymLink -> ""
                                       | otherwise -> "") in
               (case [(l,a) | OptLocation l a <- opts] of
-                    ((l,a):_) -> if a then l </> p else l
+                    ((l,a):_) -> if a then l </> p else l </> takeFileName p
                     _         -> p)
-  contents <- if isDir
-                 then return B.empty
-                 else B.readFile path
-#if MIN_VERSION_directory(1,2,0)
-  modEpochTime <- fmap (floor . utcTimeToPOSIXSeconds)
-                   $ getModificationTime path
-#else
-  (TOD modEpochTime _) <- getModificationTime path
+  contents <-
+#ifndef _WINDOWS
+              if isSymLink
+                 then do
+                   linkTarget <- readSymbolicLink path
+                   return $ C.pack linkTarget
+                 else
 #endif
+                   if isDir
+                      then
+                        return B.empty
+                      else
+                        B.fromStrict <$> S.readFile path
+  modTime <- getModificationTime path
+  tzone <- getTimeZone modTime
+  let modEpochTime = -- UNIX time computed relative to LOCAL time zone! (#67)
+        floor (utcTimeToPOSIXSeconds modTime) +
+          fromIntegral (timeZoneMinutes tzone * 60)
   let entry = toEntry path' modEpochTime contents
+
+  entryE <-
+#ifdef _WINDOWS
+        return $ entry { eVersionMadeBy = 0x0000 } -- FAT/VFAT/VFAT32 file attributes
+#else
+        do
+           let fm = if isSymLink
+                      then unionFileModes symbolicLinkMode (fileMode fs)
+                      else fileMode fs
+
+           let modes = fromIntegral $ shiftL (toInteger fm) 16
+           return $ entry { eExternalFileAttributes = modes,
+                            eVersionMadeBy = 0x0300 } -- UNIX file attributes
+#endif
+
   when (OptVerbose `elem` opts) $ do
-    let compmethod = case eCompressionMethod entry of
-                     Deflate       -> "deflated"
+    let compmethod = case eCompressionMethod entryE of
+                     Deflate       -> ("deflated" :: String)
                      NoCompression -> "stored"
     hPutStrLn stderr $
-      printf "  adding: %s (%s %.f%%)" (eRelativePath entry)
-      compmethod (100 - (100 * compressionRatio entry))
-  return entry
+      printf "  adding: %s (%s %.f%%)" (eRelativePath entryE)
+      compmethod (100 - (100 * compressionRatio entryE))
+  return entryE
 
--- | Writes contents of an 'Entry' to a file.
+-- check path, resolving .. and . components, raising
+-- UnsafePath exception if this takes you outside of the root.
+checkPath :: FilePath -> IO ()
+checkPath fp =
+  maybe (E.throwIO (UnsafePath fp)) (\_ -> return ())
+    (resolve . splitDirectories $ fp)
+  where
+    resolve =
+      fmap reverse . foldl go (return [])
+      where
+      go acc x = do
+        xs <- acc
+        case x of
+          "."  -> return xs
+          ".." -> case xs of
+                    []     -> fail "outside of root path"
+                    (_:ys) -> return ys
+          _    -> return (x:xs)
+
+-- | Writes contents of an 'Entry' to a file.  Throws a
+-- 'CRC32Mismatch' exception if the CRC32 checksum for the entry
+-- does not match the uncompressed data.
 writeEntry :: [ZipOption] -> Entry -> IO ()
 writeEntry opts entry = do
-  let path = case [d | OptDestination d <- opts] of
-                  (x:_) -> x </> eRelativePath entry
-                  _     -> eRelativePath entry
+  when (isEncryptedEntry entry) $
+    E.throwIO $ CannotWriteEncryptedEntry (eRelativePath entry)
+  let relpath = eRelativePath entry
+  checkPath relpath
+  path <- case [d | OptDestination d <- opts] of
+             (x:_)                   -> return (x </> relpath)
+             [] | isAbsolute relpath -> E.throwIO $ UnsafePath relpath
+                | otherwise          -> return relpath
   -- create directories if needed
   let dir = takeDirectory path
   exists <- doesDirectoryExist dir
@@ -274,25 +385,84 @@
     createDirectoryIfMissing True dir
     when (OptVerbose `elem` opts) $
       hPutStrLn stderr $ "  creating: " ++ dir
-  if length path > 0 && last path == '/' -- path is a directory
+  if not (null path) && last path == '/' -- path is a directory
      then return ()
      else do
-       when (OptVerbose `elem` opts) $ do
+       when (OptVerbose `elem` opts) $
          hPutStrLn stderr $ case eCompressionMethod entry of
                                  Deflate       -> " inflating: " ++ path
                                  NoCompression -> "extracting: " ++ path
-       B.writeFile path (fromEntry entry)
+       let uncompressedData = fromEntry entry
+       if eCRC32 entry == CRC32.crc32 uncompressedData
+          then B.writeFile path uncompressedData
+          else E.throwIO $ CRC32Mismatch path
+#ifndef _WINDOWS
+       let modes = fromIntegral $ shiftR (eExternalFileAttributes entry) 16
+       when (eVersionMadeBy entry .&. 0xFF00 == 0x0300 &&
+         modes /= 0) $ setFileMode path modes
+#endif
   -- Note that last modified times are supported only for POSIX, not for
   -- Windows.
   setFileTimeStamp path (eLastModified entry)
 
+#ifndef _WINDOWS
+-- | Write an 'Entry' representing a symbolic link to a file.
+-- If the 'Entry' does not represent a symbolic link or
+-- the options do not contain 'OptPreserveSymbolicLinks`, this
+-- function behaves like `writeEntry`.
+writeSymbolicLinkEntry :: [ZipOption] -> Entry -> IO ()
+writeSymbolicLinkEntry opts entry =
+  if OptPreserveSymbolicLinks `notElem` opts
+     then writeEntry opts entry
+     else do
+        if isEntrySymbolicLink entry
+           then do
+             let prefixPath = case [d | OptDestination d <- opts] of
+                                   (x:_) -> x
+                                   _     -> ""
+             let targetPath = fromJust . symbolicLinkEntryTarget $ entry
+             let symlinkPath = prefixPath </> eRelativePath entry
+             when (OptVerbose `elem` opts) $ do
+               hPutStrLn stderr $ "linking " ++ symlinkPath ++ " to " ++ targetPath
+             forceSymLink targetPath symlinkPath
+           else writeEntry opts entry
+
+
+-- | Writes a symbolic link, but removes any conflicting files and retries if necessary.
+forceSymLink :: FilePath -> FilePath -> IO ()
+forceSymLink target linkName =
+    createSymbolicLink target linkName `E.catch`
+      (\e -> if isAlreadyExistsError e
+             then removeLink linkName >> createSymbolicLink target linkName
+             else ioError e)
+
+-- | Get the target of a 'Entry' representing a symbolic link. This might fail
+-- if the 'Entry' does not represent a symbolic link
+symbolicLinkEntryTarget :: Entry -> Maybe FilePath
+symbolicLinkEntryTarget entry | isEntrySymbolicLink entry = Just . C.unpack $ fromEntry entry
+                              | otherwise = Nothing
+
+-- | Check if an 'Entry' represents a symbolic link
+isEntrySymbolicLink :: Entry -> Bool
+isEntrySymbolicLink entry = entryCMode entry .&. symbolicLinkMode == symbolicLinkMode
+
+-- | Get the 'eExternalFileAttributes' of an 'Entry' as a 'CMode' a.k.a. 'FileMode'
+entryCMode :: Entry -> CMode
+entryCMode entry = CMode (fromIntegral $ shiftR (eExternalFileAttributes entry) 16)
+#endif
+
 -- | Add the specified files to an 'Archive'.  If 'OptRecursive' is specified,
--- recursively add files contained in directories.  If 'OptVerbose' is specified,
+-- recursively add files contained in directories. if 'OptPreserveSymbolicLinks'
+-- is specified, don't recurse into it. If 'OptVerbose' is specified,
 -- print messages to stderr.
 addFilesToArchive :: [ZipOption] -> Archive -> [FilePath] -> IO Archive
 addFilesToArchive opts archive files = do
   filesAndChildren <- if OptRecursive `elem` opts
+#ifdef _WINDOWS
                          then mapM getDirectoryContentsRecursive files >>= return . nub . concat
+#else
+                         then nub . concat <$> mapM (getDirectoryContentsRecursive' opts) files
+#endif
                          else return files
   entries <- mapM (readEntry opts) filesAndChildren
   return $ foldr addEntryToArchive archive entries
@@ -301,9 +471,20 @@
 -- as needed.  If 'OptVerbose' is specified, print messages to stderr.
 -- Note that the last-modified time is set correctly only in POSIX,
 -- not in Windows.
+-- This function fails if encrypted entries are present
 extractFilesFromArchive :: [ZipOption] -> Archive -> IO ()
-extractFilesFromArchive opts archive =
-  mapM_ (writeEntry opts) $ zEntries archive
+extractFilesFromArchive opts archive = do
+  let entries = zEntries archive
+  if OptPreserveSymbolicLinks `elem` opts
+    then do
+#ifdef _WINDOWS
+      mapM_ (writeEntry opts) entries
+#else
+      let (symbolicLinkEntries, nonSymbolicLinkEntries) = partition isEntrySymbolicLink entries
+      mapM_ (writeEntry opts) nonSymbolicLinkEntries
+      mapM_ (writeSymbolicLinkEntry opts) symbolicLinkEntries
+#endif
+    else mapM_ (writeEntry opts) entries
 
 --------------------------------------------------------------------------------
 -- Internal functions for reading and writing zip binary format.
@@ -313,7 +494,13 @@
 normalizePath path =
   let dir   = takeDirectory path
       fn    = takeFileName path
-      (_drive, dir') = splitDrive dir
+      dir' = case dir of
+#ifdef _WINDOWS
+               (c:':':d:xs) | isLetter c
+                            , d == '/' || d == '\\'
+                            -> xs  -- remove drive
+#endif
+               _ -> dir
       -- note: some versions of filepath return ["."] if no dir
       dirParts = filter (/=".") $ splitDirectories dir'
   in  intercalate "/" (dirParts ++ [fn])
@@ -332,6 +519,39 @@
 decompressData Deflate       = Zlib.decompress
 decompressData NoCompression = id
 
+-- | Decrypt a lazy bytestring
+-- Returns Nothing if password is incorrect
+decryptData :: String -> EncryptionMethod -> B.ByteString -> Maybe B.ByteString
+decryptData _ NoEncryption s = Just s
+decryptData password (PKWAREEncryption controlByte) s =
+  let headerlen = 12
+      initKeys = (305419896, 591751049, 878082192)
+      startKeys = B.foldl pkwareUpdateKeys initKeys (C.pack password)
+      (header, content) = B.splitAt headerlen $ snd $ B.mapAccumL pkwareDecryptByte startKeys s
+  in if B.last header == controlByte
+        then Just content
+        else Nothing
+
+-- | PKWARE decryption context
+type DecryptionCtx = (Word32, Word32, Word32)
+
+-- | An interation of the PKWARE decryption algorithm
+pkwareDecryptByte :: DecryptionCtx -> Word8 -> (DecryptionCtx, Word8)
+pkwareDecryptByte keys@(_, _, key2) inB =
+  let tmp = key2 .|. 2
+      tmp' = fromIntegral ((tmp * (tmp `xor` 1)) `shiftR` 8) :: Word8
+      outB = inB `xor` tmp'
+  in (pkwareUpdateKeys keys outB, outB)
+
+-- | Update decryption keys after a decrypted byte
+pkwareUpdateKeys :: DecryptionCtx -> Word8 -> DecryptionCtx
+pkwareUpdateKeys (key0, key1, key2) inB =
+  let key0' = CRC32.crc32Update (key0 `xor` 0xffffffff) [inB] `xor` 0xffffffff
+      key1' = (key1 + (key0' .&. 0xff)) * 134775813 + 1
+      key1Byte = fromIntegral (key1' `shiftR` 24) :: Word8
+      key2' = CRC32.crc32Update (key2 `xor` 0xffffffff) [key1Byte] `xor` 0xffffffff
+  in (key0', key1', key2')
+
 -- | Calculate compression ratio for an entry (for verbose output).
 compressionRatio :: Entry -> Float
 compressionRatio entry =
@@ -361,49 +581,66 @@
   epochTimeToMSDOSDateTime minMSDOSDateTime
   -- if time is earlier than minimum DOS datetime, return minimum
 epochTimeToMSDOSDateTime epochtime =
-  let ut = toUTCTime (TOD epochtime 0)
-      dosTime = toEnum $ (ctSec ut `div` 2) + shiftL (ctMin ut) 5 + shiftL (ctHour ut) 11
-      dosDate = toEnum $ ctDay ut + shiftL (fromEnum (ctMonth ut) + 1) 5 + shiftL (ctYear ut - 1980) 9
+  let
+    UTCTime
+      (toGregorian -> (fromInteger -> year, month, day))
+      (timeToTimeOfDay -> (TimeOfDay hour minutes (floor -> sec)))
+      = posixSecondsToUTCTime (fromIntegral epochtime)
+
+    dosTime = toEnum $ (sec `div` 2) + shiftL minutes 5 + shiftL hour 11
+    dosDate = toEnum $ day + shiftL month 5 + shiftL (year - 1980) 9
   in  MSDOSDateTime { msDOSDate = dosDate, msDOSTime = dosTime }
 
 -- | Convert a MSDOS datetime to a 'ClockTime'.
 msDOSDateTimeToEpochTime :: MSDOSDateTime -> Integer
-msDOSDateTimeToEpochTime (MSDOSDateTime {msDOSDate = dosDate, msDOSTime = dosTime}) =
+msDOSDateTimeToEpochTime MSDOSDateTime {msDOSDate = dosDate, msDOSTime = dosTime} =
   let seconds = fromIntegral $ 2 * (dosTime .&. 0O37)
-      minutes = fromIntegral $ (shiftR dosTime 5) .&. 0O77
+      minutes = fromIntegral $ shiftR dosTime 5 .&. 0O77
       hour    = fromIntegral $ shiftR dosTime 11
       day     = fromIntegral $ dosDate .&. 0O37
-      month   = fromIntegral $ ((shiftR dosDate 5) .&. 0O17) - 1
+      month   = fromIntegral ((shiftR dosDate 5) .&. 0O17)
       year    = fromIntegral $ shiftR dosDate 9
-      timeSinceEpoch = TimeDiff
-               { tdYear = year + 10, -- dos times since 1980, unix epoch starts 1970
-                 tdMonth = month,
-                 tdDay = day - 1,  -- dos days start from 1
-                 tdHour = hour,
-                 tdMin = minutes,
-                 tdSec = seconds,
-                 tdPicosec = 0 }
-      (TOD epochsecs _) = addToClockTime timeSinceEpoch (TOD 0 0)
-  in  epochsecs
+      utc = UTCTime (fromGregorian (1980 + year) month day) (3600 * hour + 60 * minutes + seconds)
+  in floor (utcTimeToPOSIXSeconds utc)
 
+#ifndef _WINDOWS
+getDirectoryContentsRecursive' :: [ZipOption] -> FilePath -> IO [FilePath]
+getDirectoryContentsRecursive' opts path =
+  if OptPreserveSymbolicLinks `elem` opts
+     then do
+       isDir <- doesDirectoryExist path
+       if isDir
+          then do
+            isSymLink <- fmap isSymbolicLink $ getSymbolicLinkStatus path
+            if isSymLink
+               then return [path]
+               else getDirectoryContentsRecursivelyBy (getDirectoryContentsRecursive' opts) path
+          else return [path]
+     else getDirectoryContentsRecursive path
+#endif
+
 getDirectoryContentsRecursive :: FilePath -> IO [FilePath]
 getDirectoryContentsRecursive path = do
   isDir <- doesDirectoryExist path
   if isDir
-     then do
+     then getDirectoryContentsRecursivelyBy getDirectoryContentsRecursive path
+     else return [path]
+
+getDirectoryContentsRecursivelyBy :: (FilePath -> IO [FilePath]) -> FilePath -> IO [FilePath]
+getDirectoryContentsRecursivelyBy exploreMethod path = do
        contents <- getDirectoryContents path
        let contents' = map (path </>) $ filter (`notElem` ["..","."]) contents
-       children <- mapM getDirectoryContentsRecursive contents'
+       children <- mapM exploreMethod contents'
        if path == "."
           then return (concat children)
           else return (path : concat children)
-     else return [path]
 
+
 setFileTimeStamp :: FilePath -> Integer -> IO ()
-setFileTimeStamp file epochtime = do
 #ifdef _WINDOWS
-  return ()  -- TODO - figure out how to set the timestamp on Windows
+setFileTimeStamp _ _ = return () -- TODO: figure out how to set the timestamp on Windows
 #else
+setFileTimeStamp file epochtime = do
   let epochtime' = fromInteger epochtime
   setFileTimes file epochtime' epochtime'
 #endif
@@ -457,15 +694,9 @@
 
 getArchive :: Get Archive
 getArchive = do
-#if MIN_VERSION_binary(0,6,0)
-  locals <- many getLocalFile
-  files <- many (getFileHeader (M.fromList locals))
-  digSig <- Just `fmap` getDigitalSignature <|> return Nothing
-#else
   locals <- manySig 0x04034b50 getLocalFile
   files <- manySig 0x02014b50 (getFileHeader (M.fromList locals))
-  digSig <- lookAheadM getDigitalSignature
-#endif 
+  digSig <- Just `fmap` getDigitalSignature <|> return Nothing
   endSig <- getWord32le
   unless (endSig == 0x06054b50)
     $ fail "Did not find end of central directory signature"
@@ -477,7 +708,7 @@
   skip 4 -- offset of central directory
   commentLength <- getWord16le
   zipComment <- getLazyByteString (toEnum $ fromEnum commentLength)
-  return $ Archive
+  return Archive
            { zEntries                = files
            , zSignature              = digSig
            , zComment                = zipComment
@@ -489,7 +720,7 @@
   let localFileSizes = map localFileSize $ zEntries archive
   let offsets = scanl (+) 0 localFileSizes
   let cdOffset = last offsets
-  _ <- zipWithM putFileHeader offsets (zEntries archive)
+  _ <- zipWithM_ putFileHeader offsets (zEntries archive)
   putDigitalSignature $ zSignature archive
   putWord32le 0x06054b50
   putWord16le 0 -- disk number
@@ -543,7 +774,11 @@
   getWord32le >>= ensure (== 0x04034b50)
   skip 2  -- version
   bitflag <- getWord16le
-  skip 2  -- compressionMethod
+  rawCompressionMethod <- getWord16le
+  compressionMethod <- case rawCompressionMethod of
+                        0 -> return NoCompression
+                        8 -> return Deflate
+                        _ -> fail $ "Unknown compression method " ++ show rawCompressionMethod
   skip 2  -- last mod file time
   skip 2  -- last mod file date
   skip 4  -- crc32
@@ -555,39 +790,26 @@
   extraFieldLength <- getWord16le
   skip (fromIntegral fileNameLength)  -- filename
   skip (fromIntegral extraFieldLength) -- extra field
-  compressedData <- if bitflag .&. 0O10 == 0
+  compressedData <-
+    if bitflag .&. 0O10 == 0
       then getLazyByteString (fromIntegral compressedSize)
       else -- If bit 3 of general purpose bit flag is set,
            -- then we need to read until we get to the
-           -- data descriptor record.  We assume that the
-           -- record has signature 0x08074b50; this is not required
-           -- by the specification but is common.
-           do raw <- getWordsTilSig 0x08074b50
+           -- data descriptor record.
+           do raw <- getCompressedData compressionMethod
+              sig <- lookAhead getWord32le
+              when (sig == 0x08074b50) $ skip 4
               skip 4 -- crc32
               cs <- getWord32le  -- compressed size
               skip 4 -- uncompressed size
-              if fromIntegral cs == length raw
-                 then return $ B.pack raw
-                 else fail "Content size mismatch in data descriptor record" 
+              if fromIntegral cs == B.length raw
+                 then return raw
+                 else fail $ printf
+                       ("Content size mismatch in data descriptor record: "
+                         <> "expected %d, got %d bytes")
+                       cs (B.length raw)
   return (fromIntegral offset, compressedData)
 
-getWordsTilSig :: Word32 -> Get [Word8]
-getWordsTilSig sig = go []
-  where
-    go acc = do
-#if MIN_VERSION_binary(0, 6, 0)
-      (getWord32le >>= ensure (== sig) >> return (reverse acc)) <|>
-        do w <- getWord8
-           go (w:acc)
-#else
-      sig' <- lookAhead getWord32le
-      if sig == sig'
-          then skip 4 >> return (reverse acc)
-          else do
-              w <- getWord8
-              go (w:acc)
-#endif
-
 putLocalFile :: Entry -> Put
 putLocalFile f = do
   putWord32le 0x04034b50
@@ -637,12 +859,12 @@
               -> Get Entry
 getFileHeader locals = do
   getWord32le >>= ensure (== 0x02014b50)
-  skip 2 -- version made by
+  vmb <- getWord16le  -- version made by
   versionNeededToExtract <- getWord8
   skip 1 -- upper byte indicates OS part of "version needed to extract"
   unless (versionNeededToExtract <= 20) $
     fail "This archive requires zip >= 2.0 to extract."
-  skip 2 -- general purpose bit flag
+  bitflag <- getWord16le
   rawCompressionMethod <- getWord16le
   compressionMethod <- case rawCompressionMethod of
                         0 -> return NoCompression
@@ -651,6 +873,12 @@
   lastModFileTime <- getWord16le
   lastModFileDate <- getWord16le
   crc32 <- getWord32le
+  encryptionMethod <- case (testBit bitflag 0, testBit bitflag 3, testBit bitflag 6) of
+                        (False, _, _) -> return NoEncryption
+                        (True, False, False) -> return $ PKWAREEncryption (fromIntegral (crc32 `shiftR` 24))
+                        (True, True, False) -> return $ PKWAREEncryption (fromIntegral (lastModFileTime `shiftR` 8))
+                        (True, _, True) -> fail "Strong encryption is not supported"
+
   compressedSize <- getWord32le
   uncompressedSize <- getWord32le
   fileNameLength <- getWord16le
@@ -663,13 +891,14 @@
   fileName <- getLazyByteString (toEnum $ fromEnum fileNameLength)
   extraField <- getLazyByteString (toEnum $ fromEnum extraFieldLength)
   fileComment <- getLazyByteString (toEnum $ fromEnum fileCommentLength)
-  compressedData <- case (M.lookup relativeOffset locals) of
+  compressedData <- case M.lookup relativeOffset locals of
                     Just x  -> return x
                     Nothing -> fail $ "Unable to find data at offset " ++
                                         show relativeOffset
-  return $ Entry
+  return Entry
             { eRelativePath            = toString fileName
             , eCompressionMethod       = compressionMethod
+            , eEncryptionMethod        = encryptionMethod
             , eLastModified            = msDOSDateTimeToEpochTime $
                                          MSDOSDateTime { msDOSDate = lastModFileDate,
                                                          msDOSTime = lastModFileTime }
@@ -678,6 +907,7 @@
             , eUncompressedSize        = uncompressedSize
             , eExtraField              = extraField
             , eFileComment             = fileComment
+            , eVersionMadeBy           = vmb
             , eInternalFileAttributes  = internalFileAttributes
             , eExternalFileAttributes  = externalFileAttributes
             , eCompressedData          = compressedData
@@ -688,7 +918,7 @@
               -> Put
 putFileHeader offset local = do
   putWord32le 0x02014b50
-  putWord16le 0  -- version made by
+  putWord16le $ eVersionMadeBy local
   putWord16le 20 -- version needed to extract (>= 2.0)
   putWord16le 0x802  -- general purpose bit flag (bit 1 = max compression, bit 11 = UTF-8)
   putWord16le $ case eCompressionMethod local of
@@ -718,22 +948,11 @@
 -- >     size of data                    2 bytes
 -- >     signature data (variable size)
 
-#if MIN_VERSION_binary(0,6,0)
 getDigitalSignature :: Get B.ByteString
 getDigitalSignature = do
   getWord32le >>= ensure (== 0x05054b50)
   sigSize <- getWord16le
   getLazyByteString (toEnum $ fromEnum sigSize)
-#else
-getDigitalSignature :: Get (Maybe B.ByteString)
-getDigitalSignature = do
-  hdrSig <- getWord32le
-  if hdrSig /= 0x05054b50
-     then return Nothing
-     else do
-        sigSize <- getWord16le
-        getLazyByteString (toEnum $ fromEnum sigSize) >>= return . Just
-#endif
 
 putDigitalSignature :: Maybe B.ByteString -> Put
 putDigitalSignature Nothing = return ()
@@ -753,3 +972,56 @@
 
 fromString :: String -> B.ByteString
 fromString = TL.encodeUtf8 . TL.pack
+
+data DecompressResult =
+    DecompressSuccess B.ByteString -- bytes remaining
+      -- (we just discard decompressed chunks, because we only
+      -- want to know where the compressed data ends)
+  | DecompressFailure ZlibInt.DecompressError
+
+getCompressedData :: CompressionMethod -> Get B.ByteString
+getCompressedData NoCompression = do
+  -- we assume there will be a signature on the data descriptor,
+  -- otherwise we have no way of identifying where the data ends!
+  -- The signature 0x08074b50 is commonly used but not required by spec.
+  let findSigPos = do
+        w1 <- getWord8
+        if w1 == 0x50
+           then do
+             w2 <- getWord8
+             if w2 == 0x4b
+                then do
+                  w3 <- getWord8
+                  if w3 == 0x07
+                     then do
+                       w4 <- getWord8
+                       if w4 == 0x08
+                          then (\x -> x - 4) <$> bytesRead
+                          else findSigPos
+                     else findSigPos
+                else findSigPos
+           else findSigPos
+  pos <- bytesRead
+  sigpos <- lookAhead findSigPos <|>
+              fail "getCompressedData can't find data descriptor signature"
+  let compressedBytes = sigpos - pos
+  getLazyByteString compressedBytes
+getCompressedData Deflate = do
+  remainingBytes <- lookAhead getRemainingLazyByteString
+  let result = ZlibInt.foldDecompressStreamWithInput
+                (\_bs res -> res)
+                DecompressSuccess
+                DecompressFailure
+                (ZlibInt.decompressST ZlibInt.rawFormat
+                 ZlibInt.defaultDecompressParams{
+                     ZlibInt.decompressAllMembers = False })
+                remainingBytes
+  case result of
+    DecompressFailure err -> fail (show err)
+    DecompressSuccess afterCompressedBytes ->
+      -- Consume the compressed bytes; we don't do anything with
+      -- the decompressed chunks. We are just decompressing as a
+      -- way of finding where the compressed data ends.
+      getLazyByteString
+        (fromIntegral (B.length remainingBytes - B.length afterCompressedBytes))
+
diff --git a/tests/test-zip-archive.hs b/tests/test-zip-archive.hs
--- a/tests/test-zip-archive.hs
+++ b/tests/test-zip-archive.hs
@@ -1,16 +1,29 @@
 {-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE ScopedTypeVariables #-}
 -- Test suite for Codec.Archive.Zip
 -- runghc Test.hs
 
 import Codec.Archive.Zip
-import System.Directory
+import Control.Monad (unless)
+import Control.Exception (try, catch, SomeException)
+import System.Directory hiding (isSymbolicLink)
 import Test.HUnit.Base
 import Test.HUnit.Text
-import System.Process
-import qualified Data.ByteString.Lazy as B
-import Control.Applicative
+import qualified Data.ByteString.Char8 as BS
+import qualified Data.ByteString.Lazy as BL
+import qualified Data.ByteString.Lazy.Char8 as BLC
 import System.Exit
+import System.IO.Temp (withTempDirectory)
 
+#ifndef _WINDOWS
+import System.FilePath.Posix
+import System.Posix.Files
+import System.Process (rawSystem)
+#else
+import System.FilePath.Windows
+#endif
+
 -- define equality for Archives so timestamps aren't distinguished if they
 -- correspond to the same MSDOS datetime.
 instance Eq Archive where
@@ -19,83 +32,278 @@
              && (all id $ zipWith (\x y -> x { eLastModified = eLastModified x `div` 2  } ==
                                            y { eLastModified = eLastModified y `div` 2  }) (zEntries a1) (zEntries a2))
 
+#ifndef _WINDOWS
+
+createTestDirectoryWithSymlinks :: FilePath -> FilePath -> IO FilePath
+createTestDirectoryWithSymlinks prefixDir  baseDir = do
+  let testDir = prefixDir </> baseDir
+  createDirectoryIfMissing True testDir
+  createDirectoryIfMissing True (testDir </> "1")
+  writeFile (testDir </> "1/file.txt") "hello"
+  cwd <- getCurrentDirectory
+  createSymbolicLink (cwd </> testDir </> "1/file.txt") (testDir </> "link_to_file")
+  createSymbolicLink (cwd </> testDir </> "1") (testDir </> "link_to_directory")
+  return testDir
+
+#endif
+
+
+
 main :: IO Counts
-main = do
-  createDirectory "test-temp"
-  res   <- runTestTT $ TestList [ testReadWriteArchive
+main = withTempDirectory "." "test-zip-archive." $ \tmpDir -> do
+#ifndef _WINDOWS
+  ec <- catch (rawSystem "command" ["-v", "unzip"])
+         (\(_ :: SomeException) -> rawSystem "which" ["unzip"])
+  let unzipInPath = ec == ExitSuccess
+  unless unzipInPath $
+    putStrLn "\n\nunzip is not in path; skipping testArchiveAndUnzip\n"
+#endif
+  res   <- runTestTT $ TestList $ map (\f -> f tmpDir) $
+                                [ testReadWriteArchive
                                 , testReadExternalZip
                                 , testFromToArchive
                                 , testReadWriteEntry
                                 , testAddFilesOptions
                                 , testDeleteEntries
                                 , testExtractFiles
+                                , testExtractFilesFailOnEncrypted
+                                , testPasswordProtectedRead
+                                , testIncorrectPasswordRead
+                                , testEvilPath
+#ifndef _WINDOWS
+                                , testExtractFilesWithPosixAttrs
+                                , testArchiveExtractSymlinks
+                                , testExtractExternalZipWithSymlinks
+                                , testExtractOverwriteExternalZipWithSymlinks
+#endif
                                 ]
-  removeDirectoryRecursive "test-temp"
-  exitWith $ case errors res of
+#ifndef _WINDOWS
+                                ++ [testArchiveAndUnzip | unzipInPath]
+#endif
+  exitWith $ case (failures res + errors res) of
                      0 -> ExitSuccess
                      n -> ExitFailure n
 
-testReadWriteArchive :: Test
-testReadWriteArchive = TestCase $ do
+testReadWriteArchive :: FilePath -> Test
+testReadWriteArchive tmpDir = TestCase $ do
   archive <- addFilesToArchive [OptRecursive] emptyArchive ["LICENSE", "src"]
-  B.writeFile "test-temp/test1.zip" $ fromArchive archive
-  archive' <- toArchive <$> B.readFile "test-temp/test1.zip"
+  BL.writeFile (tmpDir </> "test1.zip") $ fromArchive archive
+  archive' <- toArchive <$> BL.readFile (tmpDir </> "test1.zip")
   assertEqual "for writing and reading test1.zip" archive archive'
   assertEqual "for writing and reading test1.zip" archive archive'
 
-testReadExternalZip :: Test
-testReadExternalZip = TestCase $ do
-  _ <- runCommand "zip -q -9 test-temp/test4.zip zip-archive.cabal src/Codec/Archive/Zip.hs" >>= waitForProcess
-  archive <- toArchive <$> B.readFile "test-temp/test4.zip"
+testReadExternalZip :: FilePath -> Test
+testReadExternalZip _tmpDir = TestCase $ do
+  archive <- toArchive <$> BL.readFile "tests/test4.zip"
   let files = filesInArchive archive
-  assertEqual "for results of filesInArchive" ["zip-archive.cabal", "src/Codec/Archive/Zip.hs"] files
-  cabalContents <- B.readFile "zip-archive.cabal"
-  case findEntryByPath "zip-archive.cabal" archive of 
-       Nothing  -> assertFailure "zip-archive.cabal not found in archive"
-       Just f   -> assertEqual "for contents of zip-archive.cabal in archive" cabalContents (fromEntry f)
+  assertEqual "for results of filesInArchive"
+    ["test4/","test4/a.txt","test4/b.bin","test4/c/",
+     "test4/c/with spaces.txt"] files
+  bContents <- BL.readFile "tests/test4/b.bin"
+  case findEntryByPath "test4/b.bin" archive of
+       Nothing  -> assertFailure "test4/b.bin not found in archive"
+       Just f   -> do
+                    assertEqual "for text4/b.bin file entry"
+                      NoEncryption (eEncryptionMethod f)
+                    assertEqual "for contents of test4/b.bin in archive"
+                      bContents (fromEntry f)
+  case findEntryByPath "test4/" archive of
+       Nothing  -> assertFailure "test4/ not found in archive"
+       Just f   -> assertEqual "for contents of test4/ in archive"
+                      BL.empty (fromEntry f)
 
-testFromToArchive :: Test
-testFromToArchive = TestCase $ do
-  archive <- addFilesToArchive [OptRecursive] emptyArchive ["LICENSE", "src"]
-  assertEqual "for (toArchive $ fromArchive archive)" archive (toArchive $ fromArchive archive)
+testFromToArchive :: FilePath -> Test
+testFromToArchive tmpDir = TestCase $ do
+  archive1 <- addFilesToArchive [OptRecursive] emptyArchive ["LICENSE", "src"]
+  assertEqual "for (toArchive $ fromArchive archive)" archive1 (toArchive $ fromArchive archive1)
+#ifndef _WINDOWS
+  testDir <- createTestDirectoryWithSymlinks tmpDir "test_dir_with_symlinks"
+  archive2 <- addFilesToArchive [OptRecursive, OptPreserveSymbolicLinks] emptyArchive [testDir]
+  assertEqual "for (toArchive $ fromArchive archive)" archive2 (toArchive $ fromArchive archive2)
+#endif
 
-testReadWriteEntry :: Test
-testReadWriteEntry = TestCase $ do
+testReadWriteEntry :: FilePath -> Test
+testReadWriteEntry tmpDir = TestCase $ do
   entry <- readEntry [] "zip-archive.cabal"
-  setCurrentDirectory "test-temp"
+  setCurrentDirectory tmpDir
   writeEntry [] entry
   setCurrentDirectory ".."
-  entry' <- readEntry [] "test-temp/zip-archive.cabal"
+  entry' <- readEntry [] (tmpDir </> "zip-archive.cabal")
   let entry'' = entry' { eRelativePath = eRelativePath entry, eLastModified = eLastModified entry }
   assertEqual "for readEntry -> writeEntry -> readEntry" entry entry''
 
-testAddFilesOptions :: Test
-testAddFilesOptions = TestCase $ do
+testAddFilesOptions :: FilePath -> Test
+testAddFilesOptions tmpDir = TestCase $ do
   archive1 <- addFilesToArchive [OptVerbose] emptyArchive ["LICENSE", "src"]
   archive2 <- addFilesToArchive [OptRecursive, OptVerbose] archive1 ["LICENSE", "src"]
   assertBool "for recursive and nonrecursive addFilesToArchive"
      (length (filesInArchive archive1) < length (filesInArchive archive2))
+#ifndef _WINDOWS
+  testDir <- createTestDirectoryWithSymlinks tmpDir "test_dir_with_symlinks2"
+  archive3 <- addFilesToArchive [OptVerbose, OptRecursive] emptyArchive [testDir]
+  archive4 <- addFilesToArchive [OptVerbose, OptRecursive, OptPreserveSymbolicLinks] emptyArchive [testDir]
+  mapM_ putStrLn $ filesInArchive archive3
+  mapM_ putStrLn $ filesInArchive archive4
+  assertBool "for recursive and recursive by preserving symlinks addFilesToArchive"
+     (length (filesInArchive archive4) < length (filesInArchive archive3))
+#endif
 
-testDeleteEntries :: Test
-testDeleteEntries = TestCase $ do
+
+testDeleteEntries :: FilePath -> Test
+testDeleteEntries _tmpDir = TestCase $ do
   archive1 <- addFilesToArchive [] emptyArchive ["LICENSE", "src"]
   let archive2 = deleteEntryFromArchive "LICENSE" archive1
   let archive3 = deleteEntryFromArchive "src" archive2
   assertEqual "for deleteFilesFromArchive" emptyArchive archive3
 
-testExtractFiles :: Test
-testExtractFiles = TestCase $ do
-  createDirectory "test-temp/dir1"
-  createDirectory "test-temp/dir1/dir2"
+testEvilPath :: FilePath -> Test
+testEvilPath _tmpDir = TestCase $ do
+  archive <- toArchive <$> BL.readFile "tests/zip_with_evil_path.zip"
+  result <- try $ extractFilesFromArchive [] archive :: IO (Either ZipException ())
+  case result of
+    Left err -> assertBool "Wrong exception" $ err == UnsafePath "../evil"
+    Right _ -> assertFailure "extractFilesFromArchive should have failed"
+
+testExtractFiles :: FilePath -> Test
+testExtractFiles tmpDir = TestCase $ do
+  createDirectory (tmpDir </> "dir1")
+  createDirectory (tmpDir </> "dir1/dir2")
+  let hiMsg = BS.pack "hello there"
+  let helloMsg = BS.pack "Hello there. This file is very long.  Longer than 31 characters."
+  BS.writeFile (tmpDir </> "dir1/hi") hiMsg
+  BS.writeFile (tmpDir </> "dir1/dir2/hello") helloMsg
+  archive <- addFilesToArchive [OptRecursive] emptyArchive [(tmpDir </> "dir1")]
+  removeDirectoryRecursive (tmpDir </> "dir1")
+  extractFilesFromArchive [OptVerbose] archive
+  hi <- BS.readFile (tmpDir </> "dir1/hi")
+  hello <- BS.readFile (tmpDir </> "dir1/dir2/hello")
+  assertEqual ("contents of " </> tmpDir </> "dir1/hi") hiMsg hi
+  assertEqual ("contents of " </> tmpDir </> "dir1/dir2/hello") helloMsg hello
+
+testExtractFilesFailOnEncrypted :: FilePath -> Test
+testExtractFilesFailOnEncrypted tmpDir = TestCase $ do
+  let dir = tmpDir </> "fail-encrypted"
+  createDirectory dir
+
+  archive <- toArchive <$> BL.readFile "tests/zip_with_password.zip"
+  result <- try $ extractFilesFromArchive [OptDestination dir] archive :: IO (Either ZipException ())
+  removeDirectoryRecursive dir
+
+  case result of
+    Left err -> assertBool "Wrong exception" $ err == CannotWriteEncryptedEntry "test.txt"
+    Right _ -> assertFailure "extractFilesFromArchive should have failed"
+
+testPasswordProtectedRead :: FilePath -> Test
+testPasswordProtectedRead _tmpDir = TestCase $ do
+  archive <- toArchive <$> BL.readFile "tests/zip_with_password.zip"
+
+  assertEqual "for results of filesInArchive" ["test.txt"] (filesInArchive archive)
+  case findEntryByPath "test.txt" archive of
+       Nothing  -> assertFailure "test.txt not found in archive"
+       Just f   -> do
+            assertBool "for encrypted test.txt file entry"
+              (isEncryptedEntry f)
+            assertEqual "for contents of test.txt in archive"
+              (Just $ BLC.pack "SUCCESS\n") (fromEncryptedEntry "s3cr3t" f)
+
+testIncorrectPasswordRead :: FilePath -> Test
+testIncorrectPasswordRead _tmpDir = TestCase $ do
+  archive <- toArchive <$> BL.readFile "tests/zip_with_password.zip"
+  case findEntryByPath "test.txt" archive of
+       Nothing  -> assertFailure "test.txt not found in archive"
+       Just f   -> do
+            assertEqual "for contents of test.txt in archive"
+              Nothing (fromEncryptedEntry "INCORRECT" f)
+
+#ifndef _WINDOWS
+
+testExtractFilesWithPosixAttrs :: FilePath -> Test
+testExtractFilesWithPosixAttrs tmpDir = TestCase $ do
+  createDirectory (tmpDir </> "dir3")
   let hiMsg = "hello there"
-  let helloMsg = "Hello there. This file is very long.  Longer than 31 characters."
-  writeFile "test-temp/dir1/hi" hiMsg
-  writeFile "test-temp/dir1/dir2/hello" helloMsg
-  archive <- addFilesToArchive [OptRecursive] emptyArchive ["test-temp/dir1"]
-  removeDirectoryRecursive "test-temp/dir1"
+  writeFile (tmpDir </> "dir3/hi") hiMsg
+  let perms = unionFileModes ownerReadMode $ unionFileModes ownerWriteMode ownerExecuteMode
+  setFileMode (tmpDir </> "dir3/hi") perms
+  archive <- addFilesToArchive [OptRecursive] emptyArchive [(tmpDir </> "dir3")]
+  removeDirectoryRecursive (tmpDir </> "dir3")
   extractFilesFromArchive [OptVerbose] archive
-  hi <- readFile "test-temp/dir1/hi"
-  hello <- readFile "test-temp/dir1/dir2/hello"
-  assertEqual "contents of test-temp/dir1/hi" hiMsg hi
-  assertEqual "contents of test-temp/dir1/dir2/hello" helloMsg hello
+  hi <- readFile (tmpDir </> "dir3/hi")
+  fm <- fmap fileMode $ getFileStatus (tmpDir </> "dir3/hi")
+  assertEqual "file modes" perms (intersectFileModes perms fm)
+  assertEqual ("contents of " </> tmpDir </> "dir3/hi") hiMsg hi
 
+testArchiveExtractSymlinks :: FilePath -> Test
+testArchiveExtractSymlinks tmpDir = TestCase $ do
+  testDir <- createTestDirectoryWithSymlinks tmpDir "test_dir_with_symlinks3"
+  let locationDir = "location_dir"
+  archive <- addFilesToArchive [OptRecursive, OptPreserveSymbolicLinks, OptLocation locationDir True] emptyArchive [testDir]
+  removeDirectoryRecursive testDir
+  let destination = "test_dest"
+  extractFilesFromArchive [OptPreserveSymbolicLinks, OptDestination destination] archive
+  isDirSymlink <- pathIsSymbolicLink (destination </> locationDir </> testDir </> "link_to_directory")
+  isFileSymlink <- pathIsSymbolicLink (destination </> locationDir </> testDir </> "link_to_file")
+  assertBool "Symbolic link to directory is preserved" isDirSymlink
+  assertBool "Symbolic link to file is preserved" isFileSymlink
+  removeDirectoryRecursive destination
+
+testExtractExternalZipWithSymlinks :: FilePath -> Test
+testExtractExternalZipWithSymlinks tmpDir = TestCase $ do
+  archive <- toArchive <$> BL.readFile "tests/zip_with_symlinks.zip"
+  extractFilesFromArchive [OptPreserveSymbolicLinks, OptDestination tmpDir] archive
+  let zipRootDir = "zip_test_dir_with_symlinks"
+      symlinkDir = tmpDir </> zipRootDir </> "symlink_to_dir_1"
+      symlinkFile = tmpDir </> zipRootDir </> "symlink_to_file_1"
+  isDirSymlink <- pathIsSymbolicLink symlinkDir
+  targetDirExists <- doesDirectoryExist symlinkDir
+  isFileSymlink <- pathIsSymbolicLink symlinkFile
+  targetFileExists <- doesFileExist symlinkFile
+  assertBool "Symbolic link to directory is preserved" isDirSymlink
+  assertBool "Target directory exists" targetDirExists
+  assertBool "Symbolic link to file is preserved" isFileSymlink
+  assertBool "Target file exists" targetFileExists
+  removeDirectoryRecursive tmpDir
+
+testExtractOverwriteExternalZipWithSymlinks :: FilePath -> Test
+testExtractOverwriteExternalZipWithSymlinks tmpDir = TestCase $ do
+  archive <- toArchive <$> BL.readFile "tests/zip_with_symlinks.zip"
+  extractFilesFromArchive [OptPreserveSymbolicLinks, OptDestination tmpDir] archive
+  asserts
+  extractFilesFromArchive [OptPreserveSymbolicLinks, OptDestination tmpDir] archive
+  asserts
+  where
+    zipRootDir = "zip_test_dir_with_symlinks"
+    symlinkDir = tmpDir </> zipRootDir </> "symlink_to_dir_1"
+    symlinkFile = tmpDir </> zipRootDir </> "symlink_to_file_1"
+    asserts = do
+      isDirSymlink <- pathIsSymbolicLink symlinkDir
+      targetDirExists <- doesDirectoryExist symlinkDir
+      isFileSymlink <- pathIsSymbolicLink symlinkFile
+      targetFileExists <- doesFileExist symlinkFile
+      assertBool "Symbolic link to directory is preserved" isDirSymlink
+      assertBool "Target directory exists" targetDirExists
+      assertBool "Symbolic link to file is preserved" isFileSymlink
+      assertBool "Target file exists" targetFileExists
+
+testArchiveAndUnzip :: FilePath -> Test
+testArchiveAndUnzip tmpDir = TestCase $ do
+  let dir = "test_dir_with_symlinks4"
+  testDir <- createTestDirectoryWithSymlinks tmpDir dir
+  archive <- addFilesToArchive [OptRecursive, OptPreserveSymbolicLinks] emptyArchive [testDir]
+  removeDirectoryRecursive testDir
+  let zipFile = tmpDir </> "testUnzip.zip"
+  BL.writeFile zipFile $ fromArchive archive
+  ec <- rawSystem "unzip" [zipFile]
+  assertBool "unzip succeeds" $ ec == ExitSuccess
+  let symlinkDir = testDir </> "link_to_directory"
+      symlinkFile = testDir </> "link_to_file"
+  isDirSymlink <- pathIsSymbolicLink symlinkDir
+  targetDirExists <- doesDirectoryExist symlinkDir
+  isFileSymlink <- pathIsSymbolicLink symlinkFile
+  targetFileExists <- doesFileExist symlinkFile
+  assertBool "Symbolic link to directory is preserved" isDirSymlink
+  assertBool "Target directory exists" targetDirExists
+  assertBool "Symbolic link to file is preserved" isFileSymlink
+  assertBool "Target file exists" targetFileExists
+  removeDirectoryRecursive tmpDir
+
+#endif
diff --git a/tests/test4.zip b/tests/test4.zip
new file mode 100644
Binary files /dev/null and b/tests/test4.zip differ
diff --git a/tests/test4/a.txt b/tests/test4/a.txt
new file mode 100644
--- /dev/null
+++ b/tests/test4/a.txt
@@ -0,0 +1,1 @@
+Hello, this is a test!
diff --git a/tests/test4/b.bin b/tests/test4/b.bin
new file mode 100644
--- /dev/null
+++ b/tests/test4/b.bin
@@ -0,0 +1,1 @@
+ðã¿ölÑ«>Ô0Ø^ÌÔÚªñ@¬ ùÿ
diff --git a/tests/test4/c/with spaces.txt b/tests/test4/c/with spaces.txt
new file mode 100644
--- /dev/null
+++ b/tests/test4/c/with spaces.txt
@@ -0,0 +1,1 @@
+Another file.
diff --git a/tests/zip_with_evil_path.zip b/tests/zip_with_evil_path.zip
new file mode 100644
Binary files /dev/null and b/tests/zip_with_evil_path.zip differ
diff --git a/tests/zip_with_password.zip b/tests/zip_with_password.zip
new file mode 100644
Binary files /dev/null and b/tests/zip_with_password.zip differ
diff --git a/tests/zip_with_symlinks.zip b/tests/zip_with_symlinks.zip
new file mode 100644
Binary files /dev/null and b/tests/zip_with_symlinks.zip differ
diff --git a/zip-archive.cabal b/zip-archive.cabal
--- a/zip-archive.cabal
+++ b/zip-archive.cabal
@@ -1,38 +1,70 @@
 Name:                zip-archive
-Version:             0.2.3.7
-Cabal-Version:       >= 1.10
-Build-type:          Custom
+Version:             0.4.3.2
+Cabal-Version:       2.0
+Build-type:          Simple
 Synopsis:            Library for creating and modifying zip archives.
-Description:         The zip-archive library provides functions for creating, modifying,
-                     and extracting files from zip archives.
+Description:
+   The zip-archive library provides functions for creating, modifying, and
+   extracting files from zip archives. The zip archive format is
+   documented in <http://www.pkware.com/documents/casestudies/APPNOTE.TXT>.
+   .
+   Certain simplifying assumptions are made about the zip archives: in
+   particular, there is no support for strong encryption, zip files that
+   span multiple disks, ZIP64, OS-specific file attributes, or compression
+   methods other than Deflate. However, the library should be able to read
+   the most common zip archives, and the archives it produces should be
+   readable by all standard unzip programs.
+   .
+   Archives are built and extracted in memory, so manipulating large zip
+   files will consume a lot of memory. If you work with large zip files or
+   need features not supported by this library, a better choice may be
+   <http://hackage.haskell.org/package/zip zip>, which uses a
+   memory-efficient streaming approach. However, zip can only read and
+   write archives inside instances of MonadIO, so zip-archive is a better
+   choice if you want to manipulate zip archives in "pure" contexts.
+   .
+   As an example of the use of the library, a standalone zip archiver and
+   extracter is provided in the source distribution.
 Category:            Codec
-Tested-with:         GHC == 7.4.2, GHC == 7.6.3, GHC == 7.8.2
+Tested-with:         GHC == 8.6.5, GHC == 8.8.1, GHC == 8.10.4, GHC == 9.0.1,
+                     GHC == 8.8.3, GHC == 9.2.1
 License:             BSD3
 License-file:        LICENSE
 Homepage:            http://github.com/jgm/zip-archive
 Author:              John MacFarlane
 Maintainer:          jgm@berkeley.edu
-Extra-Source-Files:  changelog, README.markdown
+Extra-Source-Files:  changelog
+                     README.markdown
+                     tests/test4.zip
+                     tests/test4/a.txt
+                     tests/test4/b.bin
+                     "tests/test4/c/with spaces.txt"
+                     tests/zip_with_symlinks.zip
+                     tests/zip_with_password.zip
+                     tests/zip_with_evil_path.zip
 
 Source-repository    head
   type:              git
   location:          git://github.com/jgm/zip-archive.git
 
-flag splitBase
-  Description:       Choose the new, smaller, split-up base package.
-  Default:           True
 flag executable
   Description:       Build the Zip executable.
   Default:           False
 
 Library
-  if flag(splitBase)
-    Build-depends:   base >= 3 && < 5, pretty, containers
-  else
-    Build-depends:   base < 3
-  Build-depends:     binary >= 0.5, zlib, filepath, bytestring >= 0.9.0,
-                     array, mtl, text >= 0.11, old-time, digest >= 0.0.0.1,
-                     directory, time
+  Build-depends:     base >= 4.5 && < 5,
+                     pretty,
+                     containers,
+                     binary >= 0.7.2,
+                     zlib,
+                     filepath,
+                     bytestring >= 0.10.0,
+                     array,
+                     mtl,
+                     text >= 0.11,
+                     digest >= 0.0.0.1,
+                     directory >= 1.2.0,
+                     time
   Exposed-modules:   Codec.Archive.Zip
   Default-Language:  Haskell98
   Hs-Source-Dirs:    src
@@ -42,15 +74,19 @@
   else
     Build-depends:   unix
 
-Executable Zip
+Executable zip-archive
   if flag(executable)
     Buildable:       True
   else
     Buildable:       False
-  Main-is:           Zip.hs
+  Main-is:           Main.hs
   Hs-Source-Dirs:    .
-  Build-Depends:     base >= 4.2 && < 5, directory >= 1.1, bytestring >= 0.9.0,
+  Build-Depends:     base >= 4.5 && < 5,
+                     directory >= 1.1,
+                     bytestring >= 0.9.0,
                      zip-archive
+  Other-Modules:     Paths_zip_archive
+  Autogen-Modules:   Paths_zip_archive
   Ghc-Options:       -Wall
   Default-Language:  Haskell98
 
@@ -58,9 +94,12 @@
   Type:           exitcode-stdio-1.0
   Main-Is:        test-zip-archive.hs
   Hs-Source-Dirs: tests
-  Build-Depends:  base >= 4.2 && < 5,
-                  directory, bytestring >= 0.9.0, process, time, old-time,
-                  HUnit, zip-archive
+  Build-Depends:  base >= 4.5 && < 5,
+                  directory >= 1.3, bytestring >= 0.9.0, process, time,
+                  HUnit, zip-archive, temporary, filepath
   Default-Language:  Haskell98
   Ghc-Options:    -Wall
-  Build-Tools:    zip
+  if os(windows)
+    cpp-options:     -D_WINDOWS
+  else
+    Build-depends:   unix
