Add zip-stream command, fixes based on testing

This commit is contained in:
Dylan Simon 2017-05-13 11:54:56 -04:00
parent ad6413d9b7
commit a7acdf1a16
7 changed files with 99 additions and 15 deletions

View File

@ -1,5 +1,6 @@
module Codec.Archive.Zip.Conduit.Internal module Codec.Archive.Zip.Conduit.Internal
( zipError ( zipVersion
, zipError
, idConduit , idConduit
, sizeCRC , sizeCRC
, sizeC , sizeC
@ -12,10 +13,14 @@ import Control.Monad.Catch (MonadThrow, throwM)
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import qualified Data.Conduit as C import qualified Data.Conduit as C
import Data.Digest.CRC32 (crc32Update) import Data.Digest.CRC32 (crc32Update)
import Data.Word (Word32, Word64) import Data.Word (Word16, Word32, Word64)
import Codec.Archive.Zip.Conduit.Types import Codec.Archive.Zip.Conduit.Types
-- |The version of this zip program, really just rough indicator of compatibility
zipVersion :: Word16
zipVersion = 48
zipError :: MonadThrow m => String -> m a zipError :: MonadThrow m => String -> m a
zipError = throwM . ZipError zipError = throwM . ZipError

View File

@ -22,7 +22,7 @@ instance Exception ZipError where
data ZipEntry = ZipEntry data ZipEntry = ZipEntry
{ zipEntryName :: ByteString -- ^File name, usually utf-8 encoded, with a trailing slash for directories { zipEntryName :: ByteString -- ^File name, usually utf-8 encoded, with a trailing slash for directories
, zipEntryTime :: LocalTime -- ^Modification time , zipEntryTime :: LocalTime -- ^Modification time
, zipEntrySize :: Maybe Word64 -- ^Size of file data (if known) , zipEntrySize :: Maybe Word64 -- ^Size of file data (if known, ignored on zip)
} }
-- |Summary information at the end of a zip stream. -- |Summary information at the end of a zip stream.

View File

@ -154,7 +154,7 @@ unZipStream = next where
centralBody sig = fail $ "Unknown header signature: " ++ show sig centralBody sig = fail $ "Unknown header signature: " ++ show sig
fileHeader = do fileHeader = do
ver <- G.getWord16le ver <- G.getWord16le
when (ver > 45) $ fail $ "Unsupported version: " ++ show ver when (ver > zipVersion) $ fail $ "Unsupported version: " ++ show ver
gpf <- G.getWord16le gpf <- G.getWord16le
when (gpf .&. complement 0o06 /= 0) $ fail $ "Unsupported flags: " ++ show gpf when (gpf .&. complement 0o06 /= 0) $ fail $ "Unsupported flags: " ++ show gpf
comp <- G.getWord16le comp <- G.getWord16le

View File

@ -2,6 +2,7 @@
{-# LANGUAGE ViewPatterns #-} {-# LANGUAGE ViewPatterns #-}
module Codec.Archive.Zip.Conduit.Zip module Codec.Archive.Zip.Conduit.Zip
( ZipOptions(..) ( ZipOptions(..)
, ZipInfo(..)
, defaultZipOptions , defaultZipOptions
, ZipEntry(..) , ZipEntry(..)
, ZipData(..) , ZipData(..)
@ -93,10 +94,6 @@ countBytes c = stateC $ \s -> c `C.fuseBoth` ((s +) <$> sizeC)
output :: MonadThrow m => P.Put -> C.ConduitM i BS.ByteString (StateT Word64 m) () output :: MonadThrow m => P.Put -> C.ConduitM i BS.ByteString (StateT Word64 m) ()
output = countBytes . sourcePut output = countBytes . sourcePut
-- |The version of this zip program, really just rough indicator of compatibility
zipVersion :: Word16
zipVersion = 48
maxBound16 :: Integral n => n maxBound16 :: Integral n => n
maxBound16 = fromIntegral (maxBound :: Word16) maxBound16 = fromIntegral (maxBound :: Word16)
@ -129,7 +126,6 @@ zipStream ZipOptions{..} = execStateC 0 $ do
mcrc = either (const Nothing) (Just . crc32) cdat mcrc = either (const Nothing) (Just . crc32) cdat
when (namelen > maxBound16) $ zipError $ BSC.unpack zipEntryName ++ ": entry name too long" when (namelen > maxBound16) $ zipError $ BSC.unpack zipEntryName ++ ": entry name too long"
let common = do let common = do
P.putWord16le $ if z64 then 45 else 20
P.putWord16le $ isLeft dat ?* bit 3 P.putWord16le $ isLeft dat ?* bit 3
P.putWord16le $ comp ?* 8 P.putWord16le $ comp ?* 8
P.putWord16le $ time P.putWord16le $ time
@ -137,6 +133,7 @@ zipStream ZipOptions{..} = execStateC 0 $ do
off <- get off <- get
output $ do output $ do
P.putWord32le 0x04034b50 P.putWord32le 0x04034b50
P.putWord16le $ if z64 then 45 else 20
common common
P.putWord32le $ fromMaybe 0 mcrc P.putWord32le $ fromMaybe 0 mcrc
P.putWord32le $ if z64 then maxBound32 else maybe 0 fromIntegral csiz P.putWord32le $ if z64 then maxBound32 else maybe 0 fromIntegral csiz
@ -166,23 +163,26 @@ zipStream ZipOptions{..} = execStateC 0 $ do
(\b -> outsz $ ((fromJust usiz, fromJust mcrc), fromJust csiz) <$ CB.sourceLbs b) (\b -> outsz $ ((fromJust usiz, fromJust mcrc), fromJust csiz) <$ CB.sourceLbs b)
cdat cdat
return $ do return $ do
-- central directory
let o64 = off >= maxBound32 let o64 = off >= maxBound32
l64 = z64 ?* 16 + o64 ?* 8 l64 = z64 ?* 16 + o64 ?* 8
a64 = z64 || o64
P.putWord32le 0x02014b50 P.putWord32le 0x02014b50
P.putWord16le zipVersion P.putWord16le zipVersion
P.putWord16le $ if a64 then 45 else 20
common common
P.putWord32le crc P.putWord32le crc
P.putWord32le $ if z64 then maxBound32 else fromIntegral csz P.putWord32le $ if z64 then maxBound32 else fromIntegral csz
P.putWord32le $ if z64 then maxBound32 else fromIntegral usz P.putWord32le $ if z64 then maxBound32 else fromIntegral usz
P.putWord16le $ fromIntegral namelen P.putWord16le $ fromIntegral namelen
P.putWord16le $ 4 + l64 P.putWord16le $ a64 ?* (4 + l64)
P.putWord16le 0 -- comment length P.putWord16le 0 -- comment length
P.putWord16le 0 -- disk number P.putWord16le 0 -- disk number
P.putWord16le 0 -- internal file attributes P.putWord16le 0 -- internal file attributes
P.putWord32le 0 -- external file attributes P.putWord32le 0 -- external file attributes
P.putWord32le $ if o64 then maxBound32 else fromIntegral off P.putWord32le $ if o64 then maxBound32 else fromIntegral off
P.putByteString zipEntryName P.putByteString zipEntryName
when (z64 || o64) $ do when a64 $ do
P.putWord16le 0x0001 P.putWord16le 0x0001
P.putWord16le l64 P.putWord16le l64
when z64 $ do when z64 $ do
@ -217,6 +217,6 @@ zipStream ZipOptions{..} = execStateC 0 $ do
P.putWord16le $ fromIntegral $ min maxBound16 cnt P.putWord16le $ fromIntegral $ min maxBound16 cnt
P.putWord16le $ fromIntegral $ min maxBound16 cnt P.putWord16le $ fromIntegral $ min maxBound16 cnt
P.putWord32le $ fromIntegral $ min maxBound32 cdlen P.putWord32le $ fromIntegral $ min maxBound32 cdlen
P.putWord32le $ fromIntegral $ max maxBound32 cdoff P.putWord32le $ fromIntegral $ min maxBound32 cdoff
P.putWord16le $ fromIntegral commlen P.putWord16le $ fromIntegral commlen
P.putByteString comment P.putByteString comment

View File

@ -7,9 +7,9 @@ import qualified Data.Conduit as C
import qualified Data.Conduit.Binary as CB import qualified Data.Conduit.Binary as CB
import Data.Time.LocalTime (localTimeToUTC, utc) import Data.Time.LocalTime (localTimeToUTC, utc)
import System.Directory (createDirectoryIfMissing, setModificationTime) import System.Directory (createDirectoryIfMissing, setModificationTime)
import System.Environment (getArgs) import System.Environment (getProgName, getArgs)
import System.Exit (exitFailure) import System.Exit (exitFailure)
import System.FilePath.Posix (takeDirectory) import System.FilePath.Posix (takeDirectory) -- zip files only use forward slashes
import System.IO (stdin, openFile, IOMode(WriteMode), hClose, hSetFileSize, hPutStrLn, stderr) import System.IO (stdin, openFile, IOMode(WriteMode), hClose, hSetFileSize, hPutStrLn, stderr)
import Codec.Archive.Zip.Conduit.UnZip import Codec.Archive.Zip.Conduit.UnZip
@ -37,9 +37,10 @@ extract = C.awaitForever start where
main :: IO () main :: IO ()
main = do main = do
prog <- getProgName
args <- getArgs args <- getArgs
unless (null args) $ do unless (null args) $ do
hPutStrLn stderr "Usage: unzip\nRead a zip file from stdin and extract it in the current directory." hPutStrLn stderr $ "Usage: " ++ prog ++ "\nRead a zip file from stdin and extract it in the current directory."
exitFailure exitFailure
ZipInfo{..} <- C.runConduit ZipInfo{..} <- C.runConduit
$ CB.sourceHandle stdin $ CB.sourceHandle stdin

62
cmd/zip.hs Normal file
View File

@ -0,0 +1,62 @@
import Control.Monad (filterM, void)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Trans.Resource (MonadResource, runResourceT)
import qualified Data.ByteString.Char8 as BSC
import qualified Data.Conduit as C
import qualified Data.Conduit.Binary as CB
import Data.List (foldl')
import Data.Time.LocalTime (utcToLocalTime, utc)
import qualified System.Console.GetOpt as Opt
import System.Directory (doesDirectoryExist, getModificationTime, isSymbolicLink, listDirectory)
import System.Environment (getProgName, getArgs)
import System.Exit (exitFailure)
import System.FilePath.Posix ((</>)) -- zip files only want forward slashes
import System.IO (stdout, hPutStrLn, stderr)
import Codec.Archive.Zip.Conduit.Zip
opts :: [Opt.OptDescr (ZipOptions -> ZipOptions)]
opts =
[ Opt.Option "z" ["compress"] (Opt.ReqArg (\l o -> o{ zipOptCompressLevel = read l }) "LEVEL")
"set compression level for files (0-9)"
, Opt.Option "0" ["store"] (Opt.NoArg (\o -> o{ zipOptCompressLevel = 0 }))
"don't compress files (-z0)"
, Opt.Option "e" ["zip64"] (Opt.NoArg (\o -> o{ zipOpt64 = True }))
"enable zip64 support for files over 4GB"
, Opt.Option "c" ["comment"] (Opt.ReqArg (\c o -> o{ zipOptInfo = (zipOptInfo o){ zipComment = BSC.pack c }}) "TEXT")
"set zip comment"
]
generate :: (MonadIO m, MonadResource m) => [FilePath] -> C.Source m (ZipEntry, ZipData m)
generate (p:paths) = do
t <- liftIO $ getModificationTime p
let e = ZipEntry
{ zipEntryName = BSC.pack $ dropWhile ('/' ==) p
, zipEntryTime = utcToLocalTime utc t -- FIXME: timezone
, zipEntrySize = Nothing
}
isd <- liftIO $ doesDirectoryExist p
if isd
then do
dl <- liftIO $ filterM (fmap not . isSymbolicLink) . map (p </>) =<< listDirectory p
C.yield (e{ zipEntryName = zipEntryName e `BSC.snoc` '/', zipEntrySize = Just 0 }, mempty)
generate $ dl ++ paths
else do
C.yield (e, zipFileData p)
generate paths
generate [] = return ()
main :: IO ()
main = do
prog <- getProgName
args <- getArgs
(opt, paths) <- case Opt.getOpt Opt.Permute opts args of
(ol, paths@(_:_), []) -> return (foldl' (flip ($)) defaultZipOptions ol, paths)
(_, _, err) -> do
mapM_ (hPutStrLn stderr) err
hPutStrLn stderr $ Opt.usageInfo ("Usage: " ++ prog ++ " [OPTION...] PATH ...\nWrite a zip file to stdout containing the given files or directories (recursively).") opts
exitFailure
runResourceT $ C.runConduit
$ generate paths
C..| void (zipStream opt)
C..| CB.sinkHandle stdout

View File

@ -54,3 +54,19 @@ executable unzip-stream
filepath, filepath,
time, time,
zip-stream zip-stream
executable zip-stream
main-is: zip.hs
hs-source-dirs: cmd
default-language: Haskell2010
ghc-options: -Wall
build-depends:
base >=4.7 && <5,
bytestring,
conduit,
conduit-extra,
directory,
filepath,
resourcet,
time,
zip-stream