339 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Haskell
		
	
	
	
	
	
		
		
			
		
	
	
			339 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Haskell
		
	
	
	
	
	
|  | {-# LANGUAGE CPP #-} | ||
|  | {-# LANGUAGE RankNTypes #-} | ||
|  | module Main where | ||
|  | 
 | ||
|  | import Codec.Compression.Zlib.Internal | ||
|  | import qualified Codec.Compression.Zlib     as Zlib | ||
|  | import qualified Codec.Compression.GZip     as GZip | ||
|  | import qualified Codec.Compression.Zlib.Raw as Raw | ||
|  | 
 | ||
|  | import Test.Codec.Compression.Zlib.Internal () | ||
|  | import Test.Codec.Compression.Zlib.Stream () | ||
|  | 
 | ||
|  | import Test.QuickCheck | ||
|  | import Test.Tasty | ||
|  | import Test.Tasty.QuickCheck | ||
|  | import Test.Tasty.HUnit | ||
|  | import Utils () | ||
|  | 
 | ||
|  | import Control.Monad | ||
|  | import Control.Monad.ST.Lazy (ST) | ||
|  | import Control.Exception | ||
|  | import qualified Data.ByteString.Char8 as BS.Char8 | ||
|  | import qualified Data.ByteString.Lazy as BL | ||
|  | import qualified Data.ByteString      as BS | ||
|  | import System.IO | ||
|  | #if !(MIN_VERSION_base(4,6,0)) | ||
|  | import Prelude hiding (catch) | ||
|  | #endif | ||
|  | 
 | ||
|  | 
 | ||
|  | main :: IO () | ||
|  | main = defaultMain $ | ||
|  |   testGroup "zlib tests" [ | ||
|  |     testGroup "property tests" [ | ||
|  |       testProperty "decompress . compress = id (standard)"           prop_decompress_after_compress, | ||
|  |       testProperty "decompress . compress = id (Zlib -> GZipOrZLib)" prop_gziporzlib1, | ||
|  |       testProperty "decompress . compress = id (GZip -> GZipOrZlib)" prop_gziporzlib2, | ||
|  |       testProperty "concatenated gzip members"                       prop_gzip_concat, | ||
|  |       testProperty "multiple gzip members, boundaries (all 2-chunks)" prop_multiple_members_boundary2, | ||
|  |       testProperty "multiple gzip members, boundaries (all 3-chunks)" prop_multiple_members_boundary3, | ||
|  |       testProperty "prefixes of valid stream detected as truncated"  prop_truncated | ||
|  |     ], | ||
|  |     testGroup "unit tests" [ | ||
|  |       testCase "simple gzip case"          test_simple_gzip, | ||
|  |       testCase "detect bad crc"            test_bad_crc, | ||
|  |       testCase "detect non-gzip"           test_non_gzip, | ||
|  |       testCase "detect custom dictionary"  test_custom_dict, | ||
|  |       testCase "dectect inflate with wrong dict"   test_wrong_dictionary, | ||
|  |       testCase "dectect inflate with right dict"   test_right_dictionary, | ||
|  |       testCase "handle trailing data"      test_trailing_data, | ||
|  |       testCase "multiple gzip members"     test_multiple_members, | ||
|  |       testCase "check small input chunks"  test_small_chunks, | ||
|  |       testCase "check empty input"         test_empty, | ||
|  |       testCase "check exception raised"    test_exception | ||
|  |     ] | ||
|  |   ] | ||
|  | 
 | ||
|  | 
 | ||
|  | prop_decompress_after_compress :: Format | ||
|  |                                -> CompressParams | ||
|  |                                -> DecompressParams | ||
|  |                                -> Property | ||
|  | prop_decompress_after_compress w cp dp = | ||
|  |    (w /= zlibFormat || decompressWindowBits dp >= compressWindowBits cp) && | ||
|  |    (decompressWindowBits dp > compressWindowBits cp) && | ||
|  |    decompressBufferSize dp > 0 && compressBufferSize cp > 0 ==> | ||
|  |    liftM2 (==) (decompress w dp . compress w cp) id | ||
|  | 
 | ||
|  | 
 | ||
|  | prop_gziporzlib1 :: CompressParams | ||
|  |                  -> DecompressParams | ||
|  |                  -> Property | ||
|  | prop_gziporzlib1 cp dp = | ||
|  |    decompressWindowBits dp > compressWindowBits cp && | ||
|  |    decompressBufferSize dp > 0 && compressBufferSize cp > 0 ==> | ||
|  |    liftM2 (==) (decompress gzipOrZlibFormat dp . compress zlibFormat cp) id | ||
|  | 
 | ||
|  | 
 | ||
|  | prop_gziporzlib2 :: CompressParams | ||
|  |                  -> DecompressParams | ||
|  |                  -> Property | ||
|  | prop_gziporzlib2 cp dp = | ||
|  |    decompressWindowBits dp >= compressWindowBits cp && | ||
|  |    decompressBufferSize dp > 0 && compressBufferSize cp > 0 ==> | ||
|  |    liftM2 (==) (decompress gzipOrZlibFormat dp . compress gzipFormat cp) id | ||
|  | 
 | ||
|  | prop_gzip_concat :: CompressParams | ||
|  |                  -> DecompressParams | ||
|  |                  -> BL.ByteString | ||
|  |                  -> Property | ||
|  | prop_gzip_concat cp dp input = | ||
|  |    decompressWindowBits dp >= compressWindowBits cp && | ||
|  |    decompressBufferSize dp > 0 && compressBufferSize cp > 0 ==> | ||
|  |    let catComp = BL.concat (replicate 5 (compress gzipFormat cp input)) | ||
|  |        compCat = compress gzipFormat cp (BL.concat (replicate 5 input)) | ||
|  | 
 | ||
|  |     in decompress gzipFormat dp { decompressAllMembers = True } catComp | ||
|  |     == decompress gzipFormat dp { decompressAllMembers = True } compCat | ||
|  | 
 | ||
|  | prop_multiple_members_boundary2 :: Property | ||
|  | prop_multiple_members_boundary2 = | ||
|  |     forAll shortStrings $ \bs -> | ||
|  |       all (\c -> decomp c == BL.append bs bs) | ||
|  |           (twoChunkSplits (comp bs `BL.append` comp bs)) | ||
|  |   where | ||
|  |     comp   = compress gzipFormat defaultCompressParams | ||
|  |     decomp = decompress gzipFormat defaultDecompressParams | ||
|  | 
 | ||
|  |     shortStrings = fmap BL.pack $ listOf arbitrary | ||
|  | 
 | ||
|  | prop_multiple_members_boundary3 :: Property | ||
|  | prop_multiple_members_boundary3 = | ||
|  |     forAll shortStrings $ \bs -> | ||
|  |       all (\c -> decomp c == BL.append bs bs) | ||
|  |           (threeChunkSplits (comp bs `BL.append` comp bs)) | ||
|  |   where | ||
|  |     comp   = compress gzipFormat defaultCompressParams | ||
|  |     decomp = decompress gzipFormat defaultDecompressParams | ||
|  | 
 | ||
|  |     shortStrings = sized $ \sz -> resize (sz `div` 10) $ | ||
|  |                    fmap BL.pack $ listOf arbitrary | ||
|  | 
 | ||
|  | prop_truncated :: Format -> Property | ||
|  | prop_truncated format = | ||
|  |    forAll shortStrings $ \bs -> | ||
|  |      all (truncated decomp) | ||
|  |          (init (BL.inits (comp bs))) | ||
|  |   -- All the initial prefixes of a valid compressed stream should be detected | ||
|  |   -- as truncated. | ||
|  |   where | ||
|  |     comp   = compress format defaultCompressParams | ||
|  |     decomp = decompressST format defaultDecompressParams | ||
|  |     truncated :: (forall s. DecompressStream (ST s)) -> BL.ByteString -> Bool | ||
|  |     truncated = foldDecompressStreamWithInput (\_ r -> r) (\_ -> False) | ||
|  |                   (\err -> case err of TruncatedInput -> True; _ -> False) | ||
|  | 
 | ||
|  |     shortStrings = sized $ \sz -> resize (sz `div` 6) arbitrary | ||
|  | 
 | ||
|  | 
 | ||
|  | test_simple_gzip :: Assertion | ||
|  | test_simple_gzip = | ||
|  |   withSampleData "hello.gz" $ \hnd -> | ||
|  |     let decomp = decompressIO gzipFormat defaultDecompressParams | ||
|  |      in assertDecompressOk hnd decomp | ||
|  | 
 | ||
|  | test_bad_crc :: Assertion | ||
|  | test_bad_crc = | ||
|  |   withSampleData "bad-crc.gz" $ \hnd -> do | ||
|  |     let decomp = decompressIO gzipFormat defaultDecompressParams | ||
|  |     err <- assertDecompressError hnd decomp | ||
|  |     msg <- assertDataFormatError err | ||
|  |     msg @?= "incorrect data check" | ||
|  | 
 | ||
|  | test_non_gzip :: Assertion | ||
|  | test_non_gzip = do | ||
|  |   withSampleData "not-gzip" $ \hnd -> do | ||
|  |     let decomp = decompressIO gzipFormat defaultDecompressParams | ||
|  |     err <- assertDecompressError hnd decomp | ||
|  |     msg <- assertDataFormatError err | ||
|  |     msg @?= "incorrect header check" | ||
|  | 
 | ||
|  |   withSampleData "not-gzip" $ \hnd -> do | ||
|  |     let decomp = decompressIO zlibFormat defaultDecompressParams | ||
|  |     err <- assertDecompressError hnd decomp | ||
|  |     msg <- assertDataFormatError err | ||
|  |     msg @?= "incorrect header check" | ||
|  | 
 | ||
|  |   withSampleData "not-gzip" $ \hnd -> do | ||
|  |     let decomp = decompressIO rawFormat defaultDecompressParams | ||
|  |     err <- assertDecompressError hnd decomp | ||
|  |     msg <- assertDataFormatError err | ||
|  |     msg @?= "invalid code lengths set" | ||
|  | 
 | ||
|  |   withSampleData "not-gzip" $ \hnd -> do | ||
|  |     let decomp = decompressIO gzipOrZlibFormat defaultDecompressParams | ||
|  |     err <- assertDecompressError hnd decomp | ||
|  |     msg <- assertDataFormatError err | ||
|  |     msg @?= "incorrect header check" | ||
|  | 
 | ||
|  | test_custom_dict :: Assertion | ||
|  | test_custom_dict = | ||
|  |   withSampleData "custom-dict.zlib" $ \hnd -> do | ||
|  |     let decomp = decompressIO zlibFormat defaultDecompressParams | ||
|  |     err <- assertDecompressError hnd decomp | ||
|  |     err @?= DictionaryRequired | ||
|  | 
 | ||
|  | test_wrong_dictionary :: Assertion | ||
|  | test_wrong_dictionary = do | ||
|  |   withSampleData "custom-dict.zlib" $ \hnd -> do | ||
|  |     let decomp = decompressIO zlibFormat defaultDecompressParams { | ||
|  |                                            decompressDictionary = -- wrong dict! | ||
|  |                                              Just (BS.pack [65,66,67]) | ||
|  |                                          } | ||
|  | 
 | ||
|  |     err <- assertDecompressError hnd decomp | ||
|  |     err @?= DictionaryMismatch | ||
|  | 
 | ||
|  | test_right_dictionary :: Assertion | ||
|  | test_right_dictionary = do | ||
|  |   withSampleData "custom-dict.zlib" $ \hnd -> do | ||
|  |     dict <- readSampleData "custom-dict.zlib-dict" | ||
|  |     let decomp = decompressIO zlibFormat defaultDecompressParams { | ||
|  |                                            decompressDictionary = | ||
|  |                                              Just (toStrict dict) | ||
|  |                                          } | ||
|  |     assertDecompressOk hnd decomp | ||
|  | 
 | ||
|  | test_trailing_data :: Assertion | ||
|  | test_trailing_data = | ||
|  |   withSampleData "two-files.gz" $ \hnd -> do | ||
|  |     let decomp = decompressIO gzipFormat defaultDecompressParams { | ||
|  |                    decompressAllMembers = False | ||
|  |                  } | ||
|  |     chunks <- assertDecompressOkChunks hnd decomp | ||
|  |     case chunks of | ||
|  |       [chunk] -> chunk @?= BS.Char8.pack "Test 1" | ||
|  |       _       -> assertFailure "expected single chunk" | ||
|  | 
 | ||
|  | test_multiple_members :: Assertion | ||
|  | test_multiple_members = | ||
|  |   withSampleData "two-files.gz" $ \hnd -> do | ||
|  |     let decomp = decompressIO gzipFormat defaultDecompressParams { | ||
|  |                    decompressAllMembers = True | ||
|  |                  } | ||
|  |     chunks <- assertDecompressOkChunks hnd decomp | ||
|  |     case chunks of | ||
|  |       [chunk1, | ||
|  |        chunk2] -> do chunk1 @?= BS.Char8.pack "Test 1" | ||
|  |                      chunk2 @?= BS.Char8.pack "Test 2" | ||
|  |       _       -> assertFailure "expected two chunks" | ||
|  | 
 | ||
|  | test_small_chunks :: Assertion | ||
|  | test_small_chunks = do | ||
|  |   uncompressedFile <- readSampleData "not-gzip" | ||
|  |   GZip.compress (smallChunks uncompressedFile) @?= GZip.compress uncompressedFile | ||
|  |   Zlib.compress (smallChunks uncompressedFile) @?= Zlib.compress uncompressedFile | ||
|  |   Raw.compress  (smallChunks uncompressedFile) @?= Raw.compress uncompressedFile | ||
|  | 
 | ||
|  |   GZip.decompress (smallChunks (GZip.compress uncompressedFile)) @?= uncompressedFile | ||
|  |   Zlib.decompress (smallChunks (Zlib.compress uncompressedFile)) @?= uncompressedFile | ||
|  |   Raw.decompress  (smallChunks (Raw.compress  uncompressedFile)) @?= uncompressedFile | ||
|  | 
 | ||
|  |   compressedFile   <- readSampleData "hello.gz" | ||
|  |   (GZip.decompress . smallChunks) compressedFile @?= GZip.decompress compressedFile | ||
|  | 
 | ||
|  | test_empty :: Assertion | ||
|  | test_empty = do | ||
|  |   -- Regression test to make sure we only ask for input once in the case of | ||
|  |   -- initially empty input. We previously asked for input twice before | ||
|  |   -- returning the error. | ||
|  |   let decomp = decompressIO zlibFormat defaultDecompressParams | ||
|  |   case decomp of | ||
|  |     DecompressInputRequired next -> do | ||
|  |       decomp' <- next BS.empty | ||
|  |       case decomp' of | ||
|  |         DecompressStreamError TruncatedInput -> return () | ||
|  |         _ -> assertFailure "expected truncated error" | ||
|  | 
 | ||
|  |     _ -> assertFailure "expected input" | ||
|  | 
 | ||
|  | test_exception :: Assertion | ||
|  | test_exception = | ||
|  |  (do | ||
|  |     compressedFile <- readSampleData "bad-crc.gz" | ||
|  |     _ <- evaluate (BL.length (GZip.decompress compressedFile)) | ||
|  |     assertFailure "expected exception") | ||
|  | 
 | ||
|  |   `catch` \err -> do | ||
|  |       msg <- assertDataFormatError err | ||
|  |       msg @?= "incorrect data check" | ||
|  | 
 | ||
|  | toStrict :: BL.ByteString -> BS.ByteString | ||
|  | #if MIN_VERSION_bytestring(0,10,0) | ||
|  | toStrict = BL.toStrict | ||
|  | #else | ||
|  | toStrict = BS.concat . BL.toChunks | ||
|  | #endif | ||
|  | 
 | ||
|  | ----------------------- | ||
|  | -- Chunk boundary utils | ||
|  | 
 | ||
|  | smallChunks :: BL.ByteString -> BL.ByteString | ||
|  | smallChunks = BL.fromChunks . map (\c -> BS.pack [c]) . BL.unpack | ||
|  | 
 | ||
|  | twoChunkSplits :: BL.ByteString -> [BL.ByteString] | ||
|  | twoChunkSplits bs = zipWith (\a b -> BL.fromChunks [a,b]) (BS.inits sbs) (BS.tails sbs) | ||
|  |   where | ||
|  |     sbs = toStrict bs | ||
|  | 
 | ||
|  | threeChunkSplits :: BL.ByteString -> [BL.ByteString] | ||
|  | threeChunkSplits bs = | ||
|  |     [ BL.fromChunks [a,b,c] | ||
|  |     | (a,x) <- zip (BS.inits sbs) (BS.tails sbs) | ||
|  |     , (b,c) <- zip (BS.inits x) (BS.tails x) ] | ||
|  |   where | ||
|  |     sbs = toStrict bs | ||
|  | 
 | ||
|  | -------------- | ||
|  | -- HUnit Utils | ||
|  | 
 | ||
|  | readSampleData :: FilePath -> IO BL.ByteString | ||
|  | readSampleData file = BL.readFile ("test/data/" ++ file) | ||
|  | 
 | ||
|  | withSampleData :: FilePath -> (Handle -> IO a) -> IO a | ||
|  | withSampleData file = withFile ("test/data/" ++ file) ReadMode | ||
|  | 
 | ||
|  | expected :: String -> String -> IO a | ||
|  | expected e g = assertFailure ("expected: " ++ e ++ "\nbut got: " ++ g) | ||
|  |             >> fail "" | ||
|  | 
 | ||
|  | assertDecompressOk :: Handle -> DecompressStream IO -> Assertion | ||
|  | assertDecompressOk hnd = | ||
|  |     foldDecompressStream | ||
|  |       (BS.hGet hnd 4000 >>=) | ||
|  |       (\_ r -> r) | ||
|  |       (\_ -> return ()) | ||
|  |       (\err -> expected "decompress ok" (show err)) | ||
|  | 
 | ||
|  | assertDecompressOkChunks :: Handle -> DecompressStream IO -> IO [BS.ByteString] | ||
|  | assertDecompressOkChunks hnd = | ||
|  |     foldDecompressStream | ||
|  |       (BS.hGet hnd 4000 >>=) | ||
|  |       (\chunk -> liftM (chunk:)) | ||
|  |       (\_ -> return []) | ||
|  |       (\err -> expected "decompress ok" (show err)) | ||
|  | 
 | ||
|  | assertDecompressError :: Handle -> DecompressStream IO -> IO DecompressError | ||
|  | assertDecompressError hnd = | ||
|  |     foldDecompressStream | ||
|  |       (BS.hGet hnd 4000 >>=) | ||
|  |       (\_ r -> r) | ||
|  |       (\_ -> expected "StreamError" "StreamEnd") | ||
|  |       return | ||
|  | 
 | ||
|  | assertDataFormatError :: DecompressError -> IO String | ||
|  | assertDataFormatError (DataFormatError detail) = return detail | ||
|  | assertDataFormatError _                        = assertFailure "expected DataError" | ||
|  |                                               >> return "" |