{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE ViewPatterns #-}
module Codec.Archive.Zip
(
Archive (..)
, Entry (..)
, CompressionMethod (..)
, EncryptionMethod (..)
, ZipOption (..)
, ZipException (..)
, emptyArchive
, toArchive
, toArchiveOrFail
, fromArchive
, filesInArchive
, addEntryToArchive
, deleteEntryFromArchive
, findEntryByPath
, fromEntry
, fromEncryptedEntry
, isEncryptedEntry
, toEntry
#ifndef _WINDOWS
, isEntrySymbolicLink
, symbolicLinkEntryTarget
, entryCMode
#endif
, readEntry
, writeEntry
#ifndef _WINDOWS
, writeSymbolicLinkEntry
#endif
, addFilesToArchive
, extractFilesFromArchive
) where
import Data.Time.Calendar ( toGregorian, fromGregorian )
import Data.Time.Clock ( UTCTime(..) )
import Data.Time.Clock.POSIX ( posixSecondsToUTCTime, utcTimeToPOSIXSeconds )
import Data.Time.LocalTime ( TimeOfDay(..), timeToTimeOfDay )
import Data.Bits ( shiftL, shiftR, (.&.), (.|.), xor, testBit )
import Data.Binary
import Data.Binary.Get
import Data.Binary.Put
import Data.List (nub, find, intercalate, isPrefixOf, isInfixOf)
import Data.Data (Data)
import Data.Typeable (Typeable)
import Text.Printf
import System.FilePath
import System.Directory
(doesDirectoryExist, getDirectoryContents,
createDirectoryIfMissing, getModificationTime, getCurrentDirectory,
makeAbsolute)
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, setFileMode, fileMode, getSymbolicLinkStatus, symbolicLinkMode, readSymbolicLink, isSymbolicLink, unionFileModes, createSymbolicLink )
import System.Posix.Types ( CMode(..) )
import Data.List (partition)
import Data.Maybe (fromJust)
#endif
import qualified Data.ByteString as S
import qualified Data.ByteString.Lazy as B
import qualified Data.ByteString.Lazy.Char8 as C
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Encoding as TL
import qualified Codec.Compression.Zlib.Raw as Zlib
#if !MIN_VERSION_binary(0, 6, 0)
manySig :: Word32 -> Get a -> Get [a]
manySig sig p = do
sig' <- lookAhead getWord32le
if sig == sig'
then do
r <- p
rs <- manySig sig p
return $ r : rs
else return []
#endif
data Archive = Archive
{ zEntries :: [Entry]
, zSignature :: Maybe B.ByteString
, zComment :: B.ByteString
} deriving (Read, Show)
instance Binary Archive where
put = putArchive
get = getArchive
data Entry = Entry
{ eRelativePath :: FilePath
, eCompressionMethod :: CompressionMethod
, eEncryptionMethod :: EncryptionMethod
, eLastModified :: Integer
, eCRC32 :: Word32
, eCompressedSize :: Word32
, eUncompressedSize :: Word32
, eExtraField :: B.ByteString
, eFileComment :: B.ByteString
, eVersionMadeBy :: Word16
, eInternalFileAttributes :: Word16
, eExternalFileAttributes :: Word32
, eCompressedData :: B.ByteString
} deriving (Read, Show, Eq)
data CompressionMethod = Deflate
| NoCompression
deriving (Read, Show, Eq)
data EncryptionMethod = NoEncryption
| PKWAREEncryption Word8
deriving (Read, Show, Eq)
data PKWAREVerificationType = CheckTimeByte
| CheckCRCByte
deriving (Read, Show, Eq)
data ZipOption = OptRecursive
| OptVerbose
| OptDestination FilePath
| OptLocation FilePath Bool
| OptPreserveSymbolicLinks
deriving (Read, Show, Eq)
data ZipException =
CRC32Mismatch FilePath
| UnsafePath FilePath
| CannotWriteEncryptedEntry FilePath
deriving (Show, Typeable, Data, Eq)
instance E.Exception ZipException
emptyArchive :: Archive
emptyArchive = Archive
{ zEntries = []
, zSignature = Nothing
, zComment = B.empty }
toArchive :: B.ByteString -> Archive
toArchive = decode
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
fromArchive :: Archive -> B.ByteString
fromArchive = encode
filesInArchive :: Archive -> [FilePath]
filesInArchive = map eRelativePath . zEntries
addEntryToArchive :: Entry -> Archive -> Archive
addEntryToArchive entry archive =
let archive' = deleteEntryFromArchive (eRelativePath entry) archive
oldEntries = zEntries archive'
in archive' { zEntries = entry : oldEntries }
deleteEntryFromArchive :: FilePath -> Archive -> Archive
deleteEntryFromArchive path archive =
archive { zEntries = [e | e <- zEntries archive
, not (eRelativePath e `matches` path)] }
findEntryByPath :: FilePath -> Archive -> Maybe Entry
findEntryByPath path archive =
find (\e -> path `matches` eRelativePath e) (zEntries archive)
fromEntry :: Entry -> B.ByteString
fromEntry entry =
decompressData (eCompressionMethod entry) (eCompressedData entry)
fromEncryptedEntry :: String -> Entry -> Maybe B.ByteString
fromEncryptedEntry password entry =
decompressData (eCompressionMethod entry) <$> decryptData password (eEncryptionMethod entry) (eCompressedData entry)
isEncryptedEntry :: Entry -> Bool
isEncryptedEntry entry =
case eEncryptionMethod entry of
(PKWAREEncryption _) -> True
_ -> False
toEntry :: FilePath
-> Integer
-> B.ByteString
-> Entry
toEntry path modtime contents =
let uncompressedSize = B.length contents
compressedData = compressData Deflate contents
compressedSize = B.length compressedData
(compressionMethod, finalData, finalSize) =
if uncompressedSize <= compressedSize
then (NoCompression, contents, uncompressedSize)
else (Deflate, compressedData, compressedSize)
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
, eInternalFileAttributes = 0
, eExternalFileAttributes = 0
, eCompressedData = finalData
}
readEntry :: [ZipOption] -> FilePath -> IO Entry
readEntry opts path = do
isDir <- doesDirectoryExist path
#ifdef _WINDOWS
let isSymLink = False
#else
fs <- getSymbolicLinkStatus path
let isSymLink = isSymbolicLink fs
#endif
let path' = let p = path ++ (case reverse path of
('/':_) -> ""
_ | isDir && not isSymLink -> "/"
_ | isDir && isSymLink -> ""
| otherwise -> "") in
(case [(l,a) | OptLocation l a <- opts] of
((l,a):_) -> if a then l </> p else l </> takeFileName p
_ -> p)
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
modEpochTime <- (floor . utcTimeToPOSIXSeconds) <$> getModificationTime path
let entry = toEntry path' modEpochTime contents
entryE <-
#ifdef _WINDOWS
return $ entry { eVersionMadeBy = 0x0000 }
#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 }
#endif
when (OptVerbose `elem` opts) $ do
let compmethod = case eCompressionMethod entryE of
Deflate -> "deflated"
NoCompression -> "stored"
hPutStrLn stderr $
printf " adding: %s (%s %.f%%)" (eRelativePath entryE)
compmethod (100 - (100 * compressionRatio entryE))
return entryE
writeEntry :: [ZipOption] -> Entry -> IO ()
writeEntry opts entry = do
when (isEncryptedEntry entry) $
E.throwIO $ CannotWriteEncryptedEntry (eRelativePath entry)
let path = case [d | OptDestination d <- opts] of
(x:_) -> x </> eRelativePath entry
_ -> eRelativePath entry
absPath <- makeAbsolute path
curDir <- getCurrentDirectory
let isUnsafePath = ".." `isInfixOf` absPath ||
not (curDir `isPrefixOf` absPath)
when isUnsafePath $ E.throwIO $ UnsafePath path
let dir = takeDirectory path
exists <- doesDirectoryExist dir
unless exists $ do
createDirectoryIfMissing True dir
when (OptVerbose `elem` opts) $
hPutStrLn stderr $ " creating: " ++ dir
if not (null path) && last path == '/'
then return ()
else do
when (OptVerbose `elem` opts) $
hPutStrLn stderr $ case eCompressionMethod entry of
Deflate -> " inflating: " ++ path
NoCompression -> "extracting: " ++ path
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
setFileTimeStamp path (eLastModified entry)
#ifndef _WINDOWS
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
createSymbolicLink targetPath symlinkPath
else writeEntry opts entry
symbolicLinkEntryTarget :: Entry -> Maybe FilePath
symbolicLinkEntryTarget entry | isEntrySymbolicLink entry = Just . C.unpack $ fromEntry entry
| otherwise = Nothing
isEntrySymbolicLink :: Entry -> Bool
isEntrySymbolicLink entry = entryCMode entry .&. symbolicLinkMode == symbolicLinkMode
entryCMode :: Entry -> CMode
entryCMode entry = CMode (fromIntegral $ shiftR (eExternalFileAttributes entry) 16)
#endif
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
extractFilesFromArchive :: [ZipOption] -> Archive -> IO ()
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
normalizePath :: FilePath -> String
normalizePath path =
let dir = takeDirectory path
fn = takeFileName path
(_drive, dir') = splitDrive dir
dirParts = filter (/=".") $ splitDirectories dir'
in intercalate "/" (dirParts ++ [fn])
matches :: FilePath -> FilePath -> Bool
matches fp1 fp2 = normalizePath fp1 == normalizePath fp2
compressData :: CompressionMethod -> B.ByteString -> B.ByteString
compressData Deflate = Zlib.compress
compressData NoCompression = id
decompressData :: CompressionMethod -> B.ByteString -> B.ByteString
decompressData Deflate = Zlib.decompress
decompressData NoCompression = id
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
type DecryptionCtx = (Word32, Word32, Word32)
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)
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')
compressionRatio :: Entry -> Float
compressionRatio entry =
if eUncompressedSize entry == 0
then 1
else fromIntegral (eCompressedSize entry) / fromIntegral (eUncompressedSize entry)
data MSDOSDateTime = MSDOSDateTime { msDOSDate :: Word16
, msDOSTime :: Word16
} deriving (Read, Show, Eq)
minMSDOSDateTime :: Integer
minMSDOSDateTime = 315532800
epochTimeToMSDOSDateTime :: Integer -> MSDOSDateTime
epochTimeToMSDOSDateTime epochtime | epochtime < minMSDOSDateTime =
epochTimeToMSDOSDateTime minMSDOSDateTime
epochTimeToMSDOSDateTime epochtime =
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 }
msDOSDateTimeToEpochTime :: MSDOSDateTime -> Integer
msDOSDateTimeToEpochTime MSDOSDateTime {msDOSDate = dosDate, msDOSTime = dosTime} =
let seconds = fromIntegral $ 2 * (dosTime .&. 0O37)
minutes = fromIntegral $ shiftR dosTime 5 .&. 0O77
hour = fromIntegral $ shiftR dosTime 11
day = fromIntegral $ dosDate .&. 0O37
month = fromIntegral ((shiftR dosDate 5) .&. 0O17)
year = fromIntegral $ shiftR dosDate 9
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 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 exploreMethod contents'
if path == "."
then return (concat children)
else return (path : concat children)
setFileTimeStamp :: FilePath -> Integer -> IO ()
#ifdef _WINDOWS
setFileTimeStamp _ _ = return ()
#else
setFileTimeStamp file epochtime = do
let epochtime' = fromInteger epochtime
setFileTimes file epochtime' epochtime'
#endif
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
endSig <- getWord32le
unless (endSig == 0x06054b50)
$ fail "Did not find end of central directory signature"
skip 2
skip 2
skip 2
skip 2
skip 4
skip 4
commentLength <- getWord16le
zipComment <- getLazyByteString (toEnum $ fromEnum commentLength)
return Archive
{ zEntries = files
, zSignature = digSig
, zComment = zipComment
}
putArchive :: Archive -> Put
putArchive archive = do
mapM_ putLocalFile $ zEntries archive
let localFileSizes = map localFileSize $ zEntries archive
let offsets = scanl (+) 0 localFileSizes
let cdOffset = last offsets
_ <- zipWithM_ putFileHeader offsets (zEntries archive)
putDigitalSignature $ zSignature archive
putWord32le 0x06054b50
putWord16le 0
putWord16le 0
putWord16le $ fromIntegral $ length $ zEntries archive
putWord16le $ fromIntegral $ length $ zEntries archive
putWord32le $ sum $ map fileHeaderSize $ zEntries archive
putWord32le $ fromIntegral cdOffset
putWord16le $ fromIntegral $ B.length $ zComment archive
putLazyByteString $ zComment archive
fileHeaderSize :: Entry -> Word32
fileHeaderSize f =
fromIntegral $ 4 + 2 + 2 + 2 + 2 + 2 + 2 + 4 + 4 + 4 + 2 + 2 + 2 + 2 + 2 + 4 + 4 +
fromIntegral (B.length $ fromString $ normalizePath $ eRelativePath f) +
B.length (eExtraField f) + B.length (eFileComment f)
localFileSize :: Entry -> Word32
localFileSize f =
fromIntegral $ 4 + 2 + 2 + 2 + 2 + 2 + 4 + 4 + 4 + 2 + 2 +
fromIntegral (B.length $ fromString $ normalizePath $ eRelativePath f) +
B.length (eExtraField f) + B.length (eCompressedData f)
getLocalFile :: Get (Word32, B.ByteString)
getLocalFile = do
offset <- bytesRead
getWord32le >>= ensure (== 0x04034b50)
skip 2
bitflag <- getWord16le
skip 2
skip 2
skip 2
skip 4
compressedSize <- getWord32le
when (compressedSize == 0xFFFFFFFF) $
fail "Can't read ZIP64 archive."
skip 4
fileNameLength <- getWord16le
extraFieldLength <- getWord16le
skip (fromIntegral fileNameLength)
skip (fromIntegral extraFieldLength)
compressedData <- if bitflag .&. 0O10 == 0
then getLazyByteString (fromIntegral compressedSize)
else
do raw <- getWordsTilSig 0x08074b50
skip 4
cs <- getWord32le
skip 4
if fromIntegral cs == B.length raw
then return raw
else fail "Content size mismatch in data descriptor record"
return (fromIntegral offset, compressedData)
getWordsTilSig :: Word32 -> Get B.ByteString
#if MIN_VERSION_binary(0, 6, 0)
getWordsTilSig sig = (B.fromChunks . reverse) `fmap` go Nothing []
where
sig' = S.pack [fromIntegral $ sig .&. 0xFF,
fromIntegral $ sig `shiftR` 8 .&. 0xFF,
fromIntegral $ sig `shiftR` 16 .&. 0xFF,
fromIntegral $ sig `shiftR` 24 .&. 0xFF]
chunkSize = 16384
checkChunk chunk = do
let (prefix, start) = S.breakSubstring sig' chunk
if S.null start
then return $ Right chunk
else return $ Left $ S.length prefix
go :: Maybe (Word8, Word8, Word8) -> [S.ByteString] -> Get [S.ByteString]
go prefixes acc = do
eitherChunkOrIndex <- lookAheadE $ do
chunk <- getByteString chunkSize <|> B.toStrict `fmap` getRemainingLazyByteString
case prefixes of
Just (byte3,byte2,byte1) ->
let len = S.length chunk in
if len >= 1 &&
S.pack [byte3,byte2,byte1,S.index chunk 0] == sig'
then return $ Left $ -3
else if len >= 2 &&
S.pack [byte2,byte1,S.index chunk 0,S.index chunk 1] == sig'
then return $ Left $ -2
else if len >= 3 &&
S.pack [byte1,S.index chunk 0,S.index chunk 1,S.index chunk 2] == sig'
then return $ Left $ -1
else checkChunk chunk
Nothing -> checkChunk chunk
case eitherChunkOrIndex of
Left index -> if index < 0
then do
skip (4 + index)
return $ (S.take (S.length (head acc) + index) (head acc)) : (tail acc)
else do
lastchunk <- getByteString index
skip 4
return (lastchunk:acc)
Right chunk -> if len == chunkSize
then go prefixes' (chunk:acc)
else fail $ "getWordsTilSig: signature not found before EOF"
where
len = S.length chunk
prefixes' = Just $ (S.index chunk (len - 3), S.index chunk (len - 2), S.index chunk (len - 1))
#else
getWordsTilSig sig = B.pack `fmap` go []
where
go acc = do
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
putWord16le 20
putWord16le 0x802
putWord16le $ case eCompressionMethod f of
NoCompression -> 0
Deflate -> 8
let modTime = epochTimeToMSDOSDateTime $ eLastModified f
putWord16le $ msDOSTime modTime
putWord16le $ msDOSDate modTime
putWord32le $ eCRC32 f
putWord32le $ eCompressedSize f
putWord32le $ eUncompressedSize f
putWord16le $ fromIntegral $ B.length $ fromString
$ normalizePath $ eRelativePath f
putWord16le $ fromIntegral $ B.length $ eExtraField f
putLazyByteString $ fromString $ normalizePath $ eRelativePath f
putLazyByteString $ eExtraField f
putLazyByteString $ eCompressedData f
getFileHeader :: M.Map Word32 B.ByteString
-> Get Entry
getFileHeader locals = do
getWord32le >>= ensure (== 0x02014b50)
vmb <- getWord16le
versionNeededToExtract <- getWord8
skip 1
unless (versionNeededToExtract <= 20) $
fail "This archive requires zip >= 2.0 to extract."
bitflag <- getWord16le
rawCompressionMethod <- getWord16le
compressionMethod <- case rawCompressionMethod of
0 -> return NoCompression
8 -> return Deflate
_ -> fail $ "Unknown compression method " ++ show rawCompressionMethod
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
extraFieldLength <- getWord16le
fileCommentLength <- getWord16le
skip 2
internalFileAttributes <- getWord16le
externalFileAttributes <- getWord32le
relativeOffset <- getWord32le
fileName <- getLazyByteString (toEnum $ fromEnum fileNameLength)
extraField <- getLazyByteString (toEnum $ fromEnum extraFieldLength)
fileComment <- getLazyByteString (toEnum $ fromEnum fileCommentLength)
compressedData <- case M.lookup relativeOffset locals of
Just x -> return x
Nothing -> fail $ "Unable to find data at offset " ++
show relativeOffset
return Entry
{ eRelativePath = toString fileName
, eCompressionMethod = compressionMethod
, eEncryptionMethod = encryptionMethod
, eLastModified = msDOSDateTimeToEpochTime $
MSDOSDateTime { msDOSDate = lastModFileDate,
msDOSTime = lastModFileTime }
, eCRC32 = crc32
, eCompressedSize = compressedSize
, eUncompressedSize = uncompressedSize
, eExtraField = extraField
, eFileComment = fileComment
, eVersionMadeBy = vmb
, eInternalFileAttributes = internalFileAttributes
, eExternalFileAttributes = externalFileAttributes
, eCompressedData = compressedData
}
putFileHeader :: Word32
-> Entry
-> Put
putFileHeader offset local = do
putWord32le 0x02014b50
putWord16le $ eVersionMadeBy local
putWord16le 20
putWord16le 0x802
putWord16le $ case eCompressionMethod local of
NoCompression -> 0
Deflate -> 8
let modTime = epochTimeToMSDOSDateTime $ eLastModified local
putWord16le $ msDOSTime modTime
putWord16le $ msDOSDate modTime
putWord32le $ eCRC32 local
putWord32le $ eCompressedSize local
putWord32le $ eUncompressedSize local
putWord16le $ fromIntegral $ B.length $ fromString
$ normalizePath $ eRelativePath local
putWord16le $ fromIntegral $ B.length $ eExtraField local
putWord16le $ fromIntegral $ B.length $ eFileComment local
putWord16le 0
putWord16le $ eInternalFileAttributes local
putWord32le $ eExternalFileAttributes local
putWord32le offset
putLazyByteString $ fromString $ normalizePath $ eRelativePath local
putLazyByteString $ eExtraField local
putLazyByteString $ eFileComment local
#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 ()
putDigitalSignature (Just sig) = do
putWord32le 0x05054b50
putWord16le $ fromIntegral $ B.length sig
putLazyByteString sig
ensure :: (a -> Bool) -> a -> Get ()
ensure p val =
if p val
then return ()
else fail "ensure not satisfied"
toString :: B.ByteString -> String
toString = TL.unpack . TL.decodeUtf8
fromString :: String -> B.ByteString
fromString = TL.encodeUtf8 . TL.pack