module Codec.Archive.Zip.Internal where
import Prelude hiding (readFile)
import Control.Applicative ((<$>))
import Control.Monad (unless)
import Data.ByteString (ByteString)
import qualified Data.ByteString as B (hGet, hPut, length, pack, empty)
import Data.Serialize (Get, Put, getByteString, getWord16le, getWord32le, putByteString, putWord16le, putWord32le, runPut, skip)
import Data.Time (UTCTime)
import Data.Word (Word16, Word32)
import System.IO (Handle, SeekMode(..), hFileSize, hSeek, hTell)
import Data.ByteString.UTF8 (fromString, toString)
import Codec.Archive.Zip.Util
calculateFileDataOffset :: Handle -> FileHeader -> IO Integer
calculateFileDataOffset h fh = do
lfhLength <- readLocalFileHeaderLength h fh
return $ fromIntegral (fhRelativeOffset fh) + lfhLength
localFileHeaderConstantLength :: Int
localFileHeaderConstantLength = 4 + 2 + 2 + 2 + 2 + 2 + 4 + 4 + 4 + 2 + 2
readLocalFileHeaderLength :: Handle -> FileHeader -> IO Integer
readLocalFileHeaderLength h header =
runGet' getLocalFileHeaderLength <$> hGetLocalFileHeader h header
getLocalFileHeaderLength :: Get Integer
getLocalFileHeaderLength = do
signature 0x04034b50
skip $ 2 + 2 + 2 + 2 + 2 + 4 + 4 + 4
fileNameLength <- fromIntegral <$> getWord16le
extraFieldLength <- fromIntegral <$> getWord16le
return $ fromIntegral localFileHeaderConstantLength
+ fileNameLength
+ extraFieldLength
writeLocalFileHeader :: Handle -> FileHeader -> IO ()
writeLocalFileHeader h fh =
B.hPut h . runPut $ putLocalFileHeader fh
putLocalFileHeader :: FileHeader -> Put
putLocalFileHeader fh = do
putWord32le 0x04034b50
putWord16le 20
putWord16le $ fhBitFlag fh
putWord16le compressionMethod
putWord16le $ msDOSTime modTime
putWord16le $ msDOSDate modTime
putWord32le $ fhCRC32 fh
putWord32le $ fhCompressedSize fh
putWord32le $ fhUncompressedSize fh
putWord16le . fromIntegral . B.length . fromString $ fhFileName fh
putWord16le . fromIntegral . B.length $ fhExtraField fh
putByteString . fromString $ fhFileName fh
putByteString $ fhExtraField fh
where
modTime = utcTimeToMSDOSDateTime $ fhLastModified fh
compressionMethod = case fhCompressionMethod fh of
NoCompression -> 0
Deflate -> 8
hGetLocalFileHeader :: Handle -> FileHeader -> IO ByteString
hGetLocalFileHeader h fh = do
hSeek h AbsoluteSeek offset
B.hGet h localFileHeaderConstantLength
where
offset = fromIntegral $ fhRelativeOffset fh
localFileHeaderLength :: FileHeader -> Word32
localFileHeaderLength fh =
fromIntegral $ 4 + 2 + 2 + 2 + 2 + 2 + 4 + 4 + 4 + 2 + 2
+ length (fhFileName fh) + B.length (fhExtraField fh)
data DataDescriptor = DataDescriptor
{ ddCRC32 :: Word32
, ddCompressedSize :: Word32
, ddUncompressedSize :: Word32
} deriving (Show)
writeDataDescriptor :: Handle -> DataDescriptor -> IO ()
writeDataDescriptor h dd =
B.hPut h . runPut $ putDataDescriptor dd
putDataDescriptor :: DataDescriptor -> Put
putDataDescriptor dd = do
putWord32le $ ddCRC32 dd
putWord32le $ ddCompressedSize dd
putWord32le $ ddUncompressedSize dd
data CentralDirectory = CentralDirectory
{ cdFileHeaders :: [FileHeader]
} deriving (Show)
readCentralDirectory :: Handle -> End -> IO CentralDirectory
readCentralDirectory h e =
runGet' getCentralDirectory <$> hGetCentralDirectory h e
writeCentralDirectory :: Handle -> CentralDirectory -> IO ()
writeCentralDirectory h cd =
B.hPut h . runPut $ putCentralDirectory cd
putCentralDirectory :: CentralDirectory -> Put
putCentralDirectory cd =
mapM_ putFileHeader $ cdFileHeaders cd
getCentralDirectory :: Get CentralDirectory
getCentralDirectory = do
headers <- many . maybeEmpty $ getFileHeader
return CentralDirectory { cdFileHeaders = headers }
hGetCentralDirectory :: Handle -> End -> IO ByteString
hGetCentralDirectory h e = do
hSeek h AbsoluteSeek $ fromIntegral offset
B.hGet h size
where
size = endCentralDirectorySize e
offset = endCentralDirectoryOffset e
data FileHeader = FileHeader
{ fhBitFlag :: Word16
, fhCompressionMethod :: CompressionMethod
, fhLastModified :: UTCTime
, fhCRC32 :: Word32
, fhCompressedSize :: Word32
, fhUncompressedSize :: Word32
, fhInternalFileAttributes :: Word16
, fhExternalFileAttributes :: Word32
, fhRelativeOffset :: Word32
, fhFileName :: FilePath
, fhExtraField :: ByteString
, fhFileComment :: ByteString
} deriving (Show)
data CompressionMethod = NoCompression
| Deflate
deriving (Show)
getFileHeader :: Get FileHeader
getFileHeader = do
signature 0x02014b50
skip 2
versionNeededToExtract <- getWord16le
unless (versionNeededToExtract <= 20) $
fail "This archive requires zip >= 2.0 to extract."
bitFlag <- getWord16le
rawCompressionMethod <- getWord16le
compessionMethod <- case rawCompressionMethod of
0 -> return NoCompression
8 -> return Deflate
_ -> fail $ "Unknown compression method "
++ show rawCompressionMethod
lastModFileTime <- getWord16le
lastModFileDate <- getWord16le
crc32 <- getWord32le
compressedSize <- fromIntegral <$> getWord32le
uncompressedSize <- getWord32le
fileNameLength <- fromIntegral <$> getWord16le
extraFieldLength <- fromIntegral <$> getWord16le
fileCommentLength <- fromIntegral <$> getWord16le
skip 2
internalFileAttributes <- getWord16le
externalFileAttributes <- getWord32le
relativeOffset <- fromIntegral <$> getWord32le
fileName <- getByteString fileNameLength
extraField <- getByteString extraFieldLength
fileComment <- getByteString fileCommentLength
return FileHeader
{ fhBitFlag = bitFlag
, fhCompressionMethod = compessionMethod
, fhLastModified = toUTC lastModFileDate lastModFileTime
, fhCRC32 = crc32
, fhCompressedSize = compressedSize
, fhUncompressedSize = uncompressedSize
, fhInternalFileAttributes = internalFileAttributes
, fhExternalFileAttributes = externalFileAttributes
, fhRelativeOffset = relativeOffset
, fhFileName = toString fileName
, fhExtraField = extraField
, fhFileComment = fileComment
}
where
toUTC date time =
msDOSDateTimeToUTCTime MSDOSDateTime { msDOSDate = date
, msDOSTime = time
}
putFileHeader :: FileHeader -> Put
putFileHeader fh = do
putWord32le 0x02014b50
putWord16le 0
putWord16le 20
putWord16le $ fhBitFlag fh
putWord16le compressionMethod
putWord16le $ msDOSTime modTime
putWord16le $ msDOSDate modTime
putWord32le $ fhCRC32 fh
putWord32le $ fhCompressedSize fh
putWord32le $ fhUncompressedSize fh
putWord16le . fromIntegral . B.length . fromString $ fhFileName fh
putWord16le . fromIntegral . B.length $ fhExtraField fh
putWord16le . fromIntegral . B.length $ fhFileComment fh
putWord16le 0
putWord16le $ fhInternalFileAttributes fh
putWord32le $ fhExternalFileAttributes fh
putWord32le $ fhRelativeOffset fh
putByteString . fromString $ fhFileName fh
putByteString $ fhExtraField fh
putByteString $ fhFileComment fh
where
modTime = utcTimeToMSDOSDateTime $ fhLastModified fh
compressionMethod = case fhCompressionMethod fh of
NoCompression -> 0
Deflate -> 8
fileHeaderLength :: FileHeader -> Word32
fileHeaderLength fh =
fromIntegral $ 4 + 2 + 2 + 2 + 2 + 2 + 2 + 4 + 4 + 4 + 2 + 2 + 2 + 2 + 2 + 4 + 4
+ length (fhFileName fh) + B.length (fhExtraField fh)
+ B.length (fhFileComment fh)
data End = End
{ endCentralDirectorySize :: Int
, endCentralDirectoryOffset :: Int
, endZipComment :: ByteString
} deriving (Show)
readEnd :: Handle -> IO End
readEnd h =
runGet' getEnd <$> hGetEnd h
getEnd :: Get End
getEnd = do
skip $ 2 + 2 + 2 + 2
size <- fromIntegral <$> getWord32le
offset <- fromIntegral <$> getWord32le
commentLength <- fromIntegral <$> getWord16le
comment <- getByteString commentLength
return End { endCentralDirectorySize = size
, endCentralDirectoryOffset = offset
, endZipComment = comment
}
hGetEnd :: Handle -> IO ByteString
hGetEnd h = do
hSeek h SeekFromEnd (4)
loop
where
loop = do
s <- B.hGet h 4
if s == B.pack (reverse [0x06, 0x05, 0x4b, 0x50])
then get
else next
get = do
size <- hFileSize h
offset <- hTell h
B.hGet h $ fromIntegral (size offset)
next = do
hSeek h RelativeSeek (5)
loop
writeEnd :: Handle -> Int -> Word32 -> Int -> IO ()
writeEnd h number size offset =
B.hPut h . runPut $ putEnd number size offset
putEnd :: Int -> Word32 -> Int -> Put
putEnd number size offset = do
putWord32le 0x06054b50
putWord16le 0
putWord16le 0
putWord16le $ fromIntegral number
putWord16le $ fromIntegral number
putWord32le size
putWord32le $ fromIntegral offset
putWord16le 0
putByteString B.empty