zip2tar-0.0: src/Main.hs
module Main where
import qualified Options.Applicative as OP
import System.Posix.Types (FileMode)
import qualified Codec.Archive.Zip.Unix as ZipUnix
import qualified Codec.Archive.Zip as Zip
import qualified Data.Conduit.Tar as CTar
import qualified Conduit as C
import Data.Conduit (runConduitRes, (.|))
import qualified Data.Time.Clock.POSIX as PosixTime
import qualified Data.ByteString.Char8 as BC
import qualified Data.Map as Map
import qualified Data.List as List
import qualified Data.Char as Char
import Data.Traversable (for)
import Data.Ord (comparing)
import Data.Bits ((.&.), (.|.))
import Data.Word (Word32)
import Control.Monad (join, void)
info :: OP.Parser a -> OP.ParserInfo a
info p =
OP.info
(OP.helper <*> p)
(OP.fullDesc <>
OP.progDesc "Convert ZIP to TAR archive")
data Sort = SortPosition | SortLexicographic
deriving (Eq, Ord)
parser :: OP.Parser (IO ())
parser =
pure convertZipToTar
<*> parseInfoConversion
<*>
OP.flag SortPosition SortLexicographic
(OP.long "sort-by-name" <>
OP.help
"sort files by names (default: storage order)")
<*>
OP.strArgument
(OP.metavar "INPUT" <>
OP.help "Input ZIP archive")
parseInfoConversion ::
OP.Parser (Zip.EntrySelector -> Zip.EntryDescription -> CTar.FileInfo)
parseInfoConversion =
pure fileInfoFromZipEntry
<*>
OP.option (OP.eitherReader packName)
(OP.long "user-name" <>
OP.metavar "NAME" <>
OP.value BC.empty <>
OP.help "UNIX user name for files in TAR output")
<*>
OP.option OP.auto
(OP.long "user-id" <>
OP.metavar "UID" <>
OP.value 0 <>
OP.help "UNIX user id for files in TAR output")
<*>
OP.option (OP.eitherReader packName)
(OP.long "group-name" <>
OP.metavar "NAME" <>
OP.value BC.empty <>
OP.help "UNIX group name for files in TAR output")
<*>
OP.option OP.auto
(OP.long "group-id" <>
OP.metavar "GID" <>
OP.value 0 <>
OP.help "UNIX group id for files in TAR output")
<*>
OP.flag AttributeWindows AttributeUNIX
(OP.long "use-unix-permissions" <>
OP.help
("respect UNIX file permissions of zipped files " ++
"(default: read Windows attributes)"))
packName :: String -> Either String BC.ByteString
packName name =
if all Char.isAscii name
then Right $ BC.pack name
else Left "name contains non-ASCII characters"
main :: IO ()
main = join $ OP.execParser $ info parser
convertZipToTar ::
(Zip.EntrySelector -> Zip.EntryDescription -> CTar.FileInfo) ->
Sort -> FilePath -> IO ()
convertZipToTar fileInfoFromZipEntry_ sortMode zipPath =
Zip.withArchive zipPath $ do
let sort =
case sortMode of
SortPosition -> List.sortBy (comparing (Zip.edOffset . snd))
SortLexicographic -> id
entries <- sort . Map.toList <$> Zip.getEntries
files <-
for entries $ \(selector, descr) ->
flip fmap (Zip.getEntrySource selector) $ \src ->
src
.|
(C.yield (Left $ fileInfoFromZipEntry_ selector descr)
>>
C.mapC Right)
C.liftIO $ runConduitRes $
sequence_ files
.|
void CTar.tar
.|
C.stdoutC
data AttributeSource = AttributeWindows | AttributeUNIX
deriving (Eq, Ord)
windowsReadonlyAttr :: Word32
windowsReadonlyAttr = 1
unixUserReadable :: FileMode
unixUserReadable = 0o400
unixUserWriteable :: FileMode
unixUserWriteable = 0o200
unixPermFromWindowsAttr :: Word32 -> FileMode
unixPermFromWindowsAttr attrs =
unixUserReadable
.|.
if attrs .&. windowsReadonlyAttr == 0
then unixUserWriteable
else 0
fileInfoFromZipEntry ::
BC.ByteString -> CTar.UserID ->
BC.ByteString -> CTar.GroupID ->
AttributeSource ->
Zip.EntrySelector -> Zip.EntryDescription -> CTar.FileInfo
fileInfoFromZipEntry userName userId groupName groupId attrSrc sel descr =
CTar.FileInfo {
CTar.filePath = BC.pack $ Zip.unEntrySelector sel,
CTar.fileUserId = userId,
CTar.fileUserName = userName,
CTar.fileGroupId = groupId,
CTar.fileGroupName = groupName,
CTar.fileMode =
(case attrSrc of
AttributeWindows -> unixPermFromWindowsAttr
AttributeUNIX -> ZipUnix.toFileMode) $
Zip.edExternalFileAttrs descr,
CTar.fileSize = fromIntegral $ Zip.edUncompressedSize descr,
CTar.fileType = CTar.FTNormal,
CTar.fileModTime =
fromInteger $ round $
(realToFrac $ PosixTime.utcTimeToPOSIXSeconds $ Zip.edModTime descr
:: Rational)
}