diff --git a/cabal-install/Distribution/Client/GZipUtils.hs b/cabal-install/Distribution/Client/GZipUtils.hs index ce1b7e4dec52616637b3cd223881ee4d52fe219a..f5d688faf57e3456b9ed9e20d1157e0f136a7124 100644 --- a/cabal-install/Distribution/Client/GZipUtils.hs +++ b/cabal-install/Distribution/Client/GZipUtils.hs @@ -1,3 +1,6 @@ +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE CPP #-} + ----------------------------------------------------------------------------- -- | -- Module : Distribution.Client.GZipUtils @@ -15,10 +18,15 @@ module Distribution.Client.GZipUtils ( maybeDecompress, ) where -import qualified Data.ByteString.Lazy.Internal as BS (ByteString(..)) -import Data.ByteString.Lazy (ByteString) -import Codec.Compression.GZip import Codec.Compression.Zlib.Internal +import Data.ByteString.Lazy.Internal as BS (ByteString(Empty, Chunk)) + +#if MIN_VERSION_zlib(0,6,0) +import Control.Exception (throw) +import Control.Monad (liftM) +import Control.Monad.ST.Lazy (ST, runST) +import qualified Data.ByteString as Strict +#endif -- | Attempts to decompress the `bytes' under the assumption that -- "data format" error at the very beginning of the stream means @@ -30,6 +38,39 @@ import Codec.Compression.Zlib.Internal -- <https://github.com/haskell/cabal/issues/678> -- maybeDecompress :: ByteString -> ByteString +#if MIN_VERSION_zlib(0,6,0) +maybeDecompress bytes = runST (go bytes decompressor) + where + decompressor :: DecompressStream (ST s) + decompressor = decompressST gzipOrZlibFormat defaultDecompressParams + + -- DataError at the beginning of the stream probably means that stream is + -- not compressed, so we return it as-is. + -- TODO: alternatively, we might consider looking for the two magic bytes + -- at the beginning of the gzip header. (not an option for zlib, though.) + go :: Monad m => ByteString -> DecompressStream m -> m ByteString + go cs (DecompressOutputAvailable bs k) = liftM (Chunk bs) $ go' cs =<< k + go _ (DecompressStreamEnd _bs ) = return Empty + go _ (DecompressStreamError _err ) = return bytes + go cs (DecompressInputRequired k) = go cs' =<< k c + where + (c, cs') = uncons cs + + -- Once we have received any output though we regard errors as actual errors + -- and we throw them (as pure exceptions). + -- TODO: We could (and should) avoid these pure exceptions. + go' :: Monad m => ByteString -> DecompressStream m -> m ByteString + go' cs (DecompressOutputAvailable bs k) = liftM (Chunk bs) $ go' cs =<< k + go' _ (DecompressStreamEnd _bs ) = return Empty + go' _ (DecompressStreamError err ) = throw err + go' cs (DecompressInputRequired k) = go' cs' =<< k c + where + (c, cs') = uncons cs + + uncons :: ByteString -> (Strict.ByteString, ByteString) + uncons Empty = (Strict.empty, Empty) + uncons (Chunk c cs) = (c, cs) +#else maybeDecompress bytes = foldStream $ decompressWithErrors gzipOrZlibFormat defaultDecompressParams bytes where -- DataError at the beginning of the stream probably means that stream is not compressed. @@ -42,3 +83,4 @@ maybeDecompress bytes = foldStream $ decompressWithErrors gzipOrZlibFormat defau doFold StreamEnd = BS.Empty doFold (StreamChunk bs stream) = BS.Chunk bs (doFold stream) doFold (StreamError _ msg) = error $ "Codec.Compression.Zlib: " ++ msg +#endif diff --git a/cabal-install/cabal-install.cabal b/cabal-install/cabal-install.cabal index c42e1c1a316316a839aa65f9eed33f71c3ca30be..f6e228a5486fd3608a87f898f40ee48336bae49a 100644 --- a/cabal-install/cabal-install.cabal +++ b/cabal-install/cabal-install.cabal @@ -141,7 +141,7 @@ executable cabal random >= 1 && < 1.2, stm >= 2.0 && < 3, time >= 1.1 && < 1.6, - zlib >= 0.5.3 && < 0.6 + zlib >= 0.5.3 && < 0.7 if flag(old-directory) build-depends: directory >= 1 && < 1.2, old-time >= 1 && < 1.2, diff --git a/cabal-install/tests/UnitTests.hs b/cabal-install/tests/UnitTests.hs index 28ee60fe553488987981d6d390e64df929e55bd1..05783ec84f7d4a03a55c9b11cad54563ee4bed6c 100644 --- a/cabal-install/tests/UnitTests.hs +++ b/cabal-install/tests/UnitTests.hs @@ -7,6 +7,7 @@ import Test.Tasty.Options import qualified UnitTests.Distribution.Client.Sandbox import qualified UnitTests.Distribution.Client.UserConfig import qualified UnitTests.Distribution.Client.Targets +import qualified UnitTests.Distribution.Client.GZipUtils import qualified UnitTests.Distribution.Client.Dependency.Modular.PSQ import qualified UnitTests.Distribution.Client.Dependency.Modular.Solver @@ -18,6 +19,8 @@ tests = testGroup "Unit Tests" [ UnitTests.Distribution.Client.Sandbox.tests ,testGroup "Distribution.Client.Targets" UnitTests.Distribution.Client.Targets.tests + ,testGroup "Distribution.Client.GZipUtils" + UnitTests.Distribution.Client.GZipUtils.tests ,testGroup "UnitTests.Distribution.Client.Dependency.Modular.PSQ" UnitTests.Distribution.Client.Dependency.Modular.PSQ.tests ,testGroup "UnitTests.Distribution.Client.Dependency.Modular.Solver" diff --git a/cabal-install/tests/UnitTests/Distribution/Client/GZipUtils.hs b/cabal-install/tests/UnitTests/Distribution/Client/GZipUtils.hs new file mode 100644 index 0000000000000000000000000000000000000000..19808233db4eefd7decca5c3320a26dc1650e3c6 --- /dev/null +++ b/cabal-install/tests/UnitTests/Distribution/Client/GZipUtils.hs @@ -0,0 +1,62 @@ +module UnitTests.Distribution.Client.GZipUtils ( + tests + ) where + +import Codec.Compression.GZip as GZip +import Codec.Compression.Zlib as Zlib +import Control.Exception.Base (evaluate) +import Control.Exception (try, SomeException) +import Control.Monad (void) +import Data.ByteString as BS (null) +import Data.ByteString.Lazy as BSL (pack, toChunks) +import Data.ByteString.Lazy.Char8 as BSLL (pack, init, length) +import Data.Monoid ((<>)) +import Distribution.Client.GZipUtils (maybeDecompress) +import Data.Word (Word8) + +import Test.Tasty +import Test.Tasty.HUnit +import Test.Tasty.QuickCheck + +tests :: [TestTree] +tests = [ testCase "maybeDecompress" maybeDecompressUnitTest + , testProperty "decompress plain" prop_maybeDecompress_plain + , testProperty "decompress zlib" prop_maybeDecompress_zlib + , testProperty "decompress gzip" prop_maybeDecompress_gzip + ] + +maybeDecompressUnitTest :: Assertion +maybeDecompressUnitTest = + assertBool "decompress plain" (maybeDecompress original == original) + >> assertBool "decompress zlib (with show)" (show (maybeDecompress compressedZlib) == show original) + >> assertBool "decompress gzip (with show)" (show (maybeDecompress compressedGZip) == show original) + >> assertBool "decompress zlib" (maybeDecompress compressedZlib == original) + >> assertBool "decompress gzip" (maybeDecompress compressedGZip == original) + >> assertBool "have no empty chunks" (Prelude.all (not . BS.null) . BSL.toChunks . maybeDecompress $ compressedZlib) + >> (runBrokenStream >>= assertBool "decompress broken stream" . isLeft) + where + original = BSLL.pack "original uncompressed input" + compressedZlib = Zlib.compress original + compressedGZip = GZip.compress original + + runBrokenStream :: IO (Either SomeException ()) + runBrokenStream = try . void . evaluate . BSLL.length $ maybeDecompress (BSLL.init compressedZlib <> BSLL.pack "*") + +prop_maybeDecompress_plain :: [Word8] -> Property +prop_maybeDecompress_plain ws = property $ maybeDecompress original == original + where original = BSL.pack ws + +prop_maybeDecompress_zlib :: [Word8] -> Property +prop_maybeDecompress_zlib ws = property $ maybeDecompress compressedZlib == original + where original = BSL.pack ws + compressedZlib = Zlib.compress original + +prop_maybeDecompress_gzip :: [Word8] -> Property +prop_maybeDecompress_gzip ws = property $ maybeDecompress compressedGZip == original + where original = BSL.pack ws + compressedGZip = GZip.compress original + +-- (Only available from "Data.Either" since 7.8.) +isLeft :: Either a b -> Bool +isLeft (Right _) = False +isLeft (Left _) = True