{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Main where
import Conduit
import Control.Exception
import Control.Monad (void, when, zipWithM_)
import Data.ByteString as S
import Data.ByteString.Char8 as S8
import Data.Conduit.List
import Data.Conduit.Tar
import Data.Int
import Data.List (sortOn)
import Data.Monoid
import Prelude as P
import System.Directory
import qualified System.FilePath as Host
import qualified System.FilePath.Posix as Posix -- always use forward slashes
import System.IO
import System.IO (BufferMode (LineBuffering),
hSetBuffering, stdout)
import Test.Hspec
import Test.QuickCheck
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative (pure, (<$>))
import Data.Word
#endif
import Data.Conduit.Zlib (ungzip)
main :: IO ()
main = do
hSetBuffering stdout LineBuffering
let baseTmp = "tar-conduit-tests"
isStack <- doesDirectoryExist ".stack-work"
let testPaths =
["src", "./tests", "README.md", "ChangeLog.md", "LICENSE"] ++
if isStack
then [".stack-work", "./sample"]
else []
hspec $ do
describe "tar/untar" $ do
let tarUntarContent dir =
runConduitRes $
yield dir .| void tarFilePath .| untar (const (foldC >>= yield)) .| foldC
it "content" $ do
c <- collectContent "src"
tarUntarContent "src" `shouldReturn` c
describe "tar/untar/tar" $ do
around (withTempTarFiles baseTmp) $
it "structure" $ \(fpIn, hIn, outDir, fpOut) -> do
writeTarball hIn testPaths
hClose hIn
extractTarball fpIn (Just outDir)
curDir <- getCurrentDirectory
finally
(setCurrentDirectory outDir >> createTarball fpOut testPaths)
(setCurrentDirectory curDir)
tb1 <- readTarballSorted fpIn
tb2 <- readTarballSorted fpOut
P.length tb1 `shouldBe` P.length tb2
zipWithM_ shouldBe (fmap fst tb2) (fmap fst tb1)
zipWithM_ shouldBe (fmap snd tb2) (fmap snd tb1)
describe "untar" $ do
around (withTempTarFiles baseTmp) $
it "create-intermediate" $ \(fpIn, hIn, outDir, fpOut) -> do
hClose hIn
extractTarballLenient "tests/files/subdir.tar" (Just outDir)
curDir <- getCurrentDirectory
collectContent (outDir Posix.</> "dir/subdir/") `shouldReturn` "Hello World\n"
describe "ustar" ustarSpec
describe "GNUtar" gnutarSpec
describe "unsupported headers" $ do
it "associated payload is discarded" $ do
contents <- readGzipTarball "./tests/files/libpq-0.3.tar.gz"
let fileNames = filePath . fst <$> contents
fileNames `shouldContain` [ "libpq-0.3/"
, "libpq-0.3/.gitignore"
, "libpq-0.3/Database/"
, "libpq-0.3/Database/PQ.hsc"
, "libpq-0.3/LICENSE"
, "libpq-0.3/README.md"
, "libpq-0.3/Setup.hs"
, "libpq-0.3/libpq.cabal"
]
defFileInfo :: FileInfo
defFileInfo =
FileInfo
{ filePath = "test-file-name"
, fileUserId = 1000
, fileUserName = "test-user-name"
, fileGroupId = 1000
, fileGroupName = "test-group-name"
, fileMode = 0o644
, fileSize = 0
, fileType = FTNormal
, fileModTime = 123456789
}
fileInfoExpectation :: [(FileInfo, ByteString)] -> IO ()
fileInfoExpectation files = do
let source = P.concat [[Left fi, Right content] | (fi, content) <- files]
collectBack fi = do
content <- foldC
yield (fi, content)
result <- runConduit $ sourceList source .| void tar .| untar collectBack .| sinkList
result `shouldBe` files
data GnuTarFile = GnuTarFile FileInfo (Maybe ByteString) deriving (Show, Eq)
asciiGen :: Int -> Gen ByteString
asciiGen n = S.pack <$> vectorOf n (frequency [(1, pure 0x2f), (20, choose (0x20, 0x7e))])
instance Arbitrary GnuTarFile where
arbitrary = do
filePathLen <- (`mod` 4090) <$> arbitrary
filePath <- ("test-" <>) . S8.filter (/= ('\\')) <$> asciiGen filePathLen
NonNegative fileUserId64 <- arbitrary
let fileUserId = fromIntegral (fileUserId64 :: Int64)
NonNegative fileGroupId64 <- arbitrary
let fileGroupId = fromIntegral (fileGroupId64 :: Int64)
fileUserNameLen <- (`mod` 32) <$> arbitrary
fileUserName <- asciiGen fileUserNameLen
fileGroupNameLen <- (`mod` 32) <$> arbitrary
fileGroupName <- asciiGen fileGroupNameLen
fileMode <- fromIntegral <$> choose (0o000 :: Word, 0o777)
-- TODO: use `filePathLen` instead, once long link name 'K' is implemented
linkNameLen <- (`mod` 101) <$> arbitrary
fileType <-
oneof
[ pure FTNormal
, pure FTDirectory
, FTSymbolicLink <$> asciiGen linkNameLen
, FTHardLink <$> asciiGen linkNameLen
]
(fileSize, mContent) <-
case fileType of
FTNormal -> do
content <- arbitraryByteString
return (fromIntegral (S.length content), Just content)
_ -> return (0, Nothing)
fileModTime <- fromIntegral <$> (arbitrary :: Gen Int64)
return (GnuTarFile FileInfo {..} mContent)
arbitraryByteString :: Gen ByteString
arbitraryByteString = do
maxLen <- arbitrary
len <- (`mod` (maxLen + 1)) <$> arbitrary
genFun <- arbitrary
let strGen x | x < len = Just (genFun x, x + 1)
| otherwise = Nothing
return $ fst $ S.unfoldrN maxLen strGen 0
fileInfoProperty :: [GnuTarFile] -> Property
fileInfoProperty files = either throw (source ===) eResult
where
eResult = runConduit $ sourceList source .| void tar .| untar collectBack .| sinkList
source =
P.concat [Left fi : maybe [] ((: []) . Right) mContent | GnuTarFile fi mContent <- files]
collectBack fi = do
yield $ Left fi
case fileType fi of
FTNormal -> do
content <- foldC
yield $ Right content
_ -> return ()
emptyFileInfoExpectation :: FileInfo -> IO ()
emptyFileInfoExpectation fi = fileInfoExpectation [(fi, "")]
ustarSpec :: Spec
ustarSpec = do
it "minimal" $ do
emptyFileInfoExpectation defFileInfo
it "long file name <255" $ do
emptyFileInfoExpectation $
defFileInfo {filePath = S8.pack (P.replicate 99 'f' Posix.</> P.replicate 99 'o')}
gnutarSpec :: Spec
gnutarSpec = do
it "LongLink - a file with long file name" $ do
emptyFileInfoExpectation $
defFileInfo
{ filePath =
S8.pack (P.replicate 100 'f' Posix.</>
P.replicate 100 'o' Posix.</>
P.replicate 99 'b')
}
it "LongLink - multiple files with long file names" $ do
fileInfoExpectation
[ ( defFileInfo
{ filePath =
S8.pack (P.replicate 100 'f' Posix.</>
P.replicate 100 'o' Posix.</>
P.replicate 99 'b')
, fileSize = 10
}
, "1234567890")
, ( defFileInfo
{ filePath =
S8.pack (P.replicate 1000 'g' Posix.</>
P.replicate 1000 'o' Posix.</>
P.replicate 99 'b')
, fileSize = 11
}
, "abcxdefghij")
]
it "Large User Id" $ do emptyFileInfoExpectation $ defFileInfo {fileUserId = 0o777777777}
it "All Large Numeric Values" $ do
emptyFileInfoExpectation $
defFileInfo
{ fileUserId = 0x7FFFFFFFFFFFFFFF
, fileGroupId = 0x7FFFFFFFFFFFFFFF
, fileModTime = fromIntegral (maxBound :: Int64)
}
it "Negative Values" $ do
emptyFileInfoExpectation $ defFileInfo {fileModTime = fromIntegral (minBound :: Int64)}
emptyFileInfoExpectation $ defFileInfo {fileModTime = -10}
emptyFileInfoExpectation $ defFileInfo {fileUserId = fromIntegral (minBound :: Int64)}
it "Negative Size" $
(emptyFileInfoExpectation (defFileInfo {fileSize = -10}) `shouldThrow`
(\case
TarCreationError _ -> True
_ -> False))
it "tar/untar Property" $ property fileInfoProperty
withTempTarFiles :: FilePath -> ((FilePath, Handle, FilePath, FilePath) -> IO c) -> IO c
withTempTarFiles base =
bracket
(do tmpDir <- getTemporaryDirectory
(fp1, h1) <- openBinaryTempFile tmpDir (Host.addExtension base ".tar")
let outPath = Host.dropExtension fp1 ++ ".out"
return (fp1, h1, outPath, Host.addExtension outPath ".tar")
)
(\(fp, h, dirOut, fpOut) -> do
hClose h
removeFile fp
doesDirectoryExist dirOut >>= (`when` removeDirectoryRecursive dirOut)
doesFileExist fpOut >>= (`when` removeFile fpOut)
)
-- | Collects all of the files and direcotries from the tarball. Then all of them get sorted, since
-- apparently Windows has no guaranteed order the files within a directory will be listed in upon a
-- tarball creation.
readTarballSorted
:: FilePath -> IO [(FileInfo, Maybe ByteString)]
readTarballSorted fp =
sortOn (filePath . fst) <$>
(runConduitRes $ sourceFileBS fp .| untar grabBoth .| sinkList)
readGzipTarball
:: FilePath
-> IO [(FileInfo, Maybe ByteString)]
readGzipTarball fp =
runConduitRes $ sourceFileBS fp .| ungzip .| untar grabBoth .| sinkList
grabBoth
:: (Monad m)
=> FileInfo
-> ConduitM ByteString (FileInfo, Maybe ByteString) m ()
grabBoth fi =
case fileType fi of
FTNormal -> do
content <- foldC
yield (fi, Just content)
_ -> yield (fi, Nothing)
collectContent :: FilePath -> IO (ByteString)
collectContent dir =
runConduitRes $
sourceDirectoryDeep False dir .| mapMC (\fp -> runConduit (sourceFileBS fp .| foldC)) .| foldC