diff --git a/.travis.yml b/.travis.yml index e7b99df..c3e7bd3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,7 +25,7 @@ env: - CABAL_NO_SANDBOX=y - CABAL_HACKAGE_MIRROR=hackage.haskell.org:http://hackage.fpcomplete.com - TEST_INSTALL=y - - PATH=/bin:/usr/bin + - PATH=/bin:/usr/bin:/opt/ghc/bin:/opt/ghc-ppa-tools/bin - LC_ALL=C.UTF-8 # ------------------------------------------------------------------------ @@ -84,6 +84,10 @@ matrix: # (Linux) cabal builds # -------------------------------------------------------------------------- + # TODO: Remove --allow-newer + - env: BUILD=cabal-new CABALVER=2.4 GHCVER=8.6 CABAL_NEWBUILD_OPTIONS="--allow-newer" + addons: {apt: {packages: [cabal-install-2.4,ghc-8.6.1], sources: [hvr-ghc]}} + - env: BUILD=cabal-new CABALVER=2.2 GHCVER=8.4.1 addons: {apt: {packages: [cabal-install-2.2,ghc-8.4.1], sources: [hvr-ghc]}} @@ -96,22 +100,6 @@ matrix: - env: BUILD=cabal-new CABALVER=2.2 GHCVER=7.10.3 addons: {apt: {packages: [cabal-install-2.2,ghc-7.10.3], sources: [hvr-ghc]}} - # Tests are disabled for GHC < 7.10, so cabal new-build doesn't fail - - env: BUILD=cabal-new CABALVER=2.2 GHCVER=7.8.4 DISABLE_TEST=y DISABLE_BENCH=y - addons: {apt: {packages: [cabal-install-2.2,ghc-7.8.4], sources: [hvr-ghc]}} - - - env: BUILD=cabal-new CABALVER=2.2 GHCVER=7.6.3 DISABLE_TEST=y DISABLE_BENCH=y - addons: {apt: {packages: [cabal-install-2.2,ghc-7.6.3], sources: [hvr-ghc]}} - - - env: BUILD=cabal-new CABALVER=2.2 GHCVER=7.4.2 DISABLE_TEST=y DISABLE_BENCH=y - addons: {apt: {packages: [cabal-install-2.2,ghc-7.4.2], sources: [hvr-ghc]}} - - - env: BUILD=cabal-new CABALVER=2.2 GHCVER=7.2.2 DISABLE_TEST=y DISABLE_BENCH=y - addons: {apt: {packages: [cabal-install-2.2,ghc-7.2.2], sources: [hvr-ghc]}} - - - env: BUILD=cabal-new CABALVER=2.2 GHCVER=7.0.4 DISABLE_TEST=y DISABLE_BENCH=y - addons: {apt: {packages: [cabal-install-2.2,ghc-7.0.4], sources: [hvr-ghc]}} - #- env: BUILD=cabal-new CABALVER=head GHCVER=head # addons: {apt: {packages: [cabal-install-head,ghc-head], sources: [hvr-ghc]}} diff --git a/CHANGELOG.md b/CHANGELOG.md index 6444afa..de3ce43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog for the `bsb-http-chunked` package +## [0.0.0.4] – 2018-09-29 + +- Fix an issue with file modification times in the tarball that prevented + installation on Windows 10. [#22] +- Remove compatibility with GHC < 7.10 (prompted by internal refactorings) + ## [0.0.0.3] – 2018-09-01 - Compatibility with GHC-8.6 @@ -23,7 +29,10 @@ Initial release. The format of this changelog is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) -[Unreleased]: https://github.com/sjakobi/bsb-http-chunked/compare/v0.0.0.3...HEAD +[Unreleased]: https://github.com/sjakobi/bsb-http-chunked/compare/v0.0.0.4...HEAD +[0.0.0.4]: https://github.com/sjakobi/bsb-http-chunked/compare/v0.0.0.3...v0.0.0.4 [0.0.0.3]: https://github.com/sjakobi/bsb-http-chunked/compare/v0.0.0.2...v0.0.0.3 [0.0.0.2]: https://github.com/sjakobi/bsb-http-chunked/compare/v0.0.0.1...v0.0.0.2 [0.0.0.1]: https://github.com/sjakobi/bsb-http-chunked/compare/v0...v0.0.0.1 + +[#22]: https://github.com/sjakobi/bsb-http-chunked/issues/22 diff --git a/Data/ByteString/Builder/HTTP/Chunked.hs b/Data/ByteString/Builder/HTTP/Chunked.hs index a914233..affd7a0 100644 --- a/Data/ByteString/Builder/HTTP/Chunked.hs +++ b/Data/ByteString/Builder/HTTP/Chunked.hs @@ -1,4 +1,4 @@ -{-# LANGUAGE BangPatterns, MagicHash, OverloadedStrings #-} +{-# LANGUAGE BangPatterns, MagicHash, OverloadedStrings, ScopedTypeVariables #-} -- | HTTP/1.1 chunked transfer encoding as defined -- in [RFC 7230 Section 4.1](https://tools.ietf.org/html/rfc7230#section-4.1) @@ -7,16 +7,14 @@ module Data.ByteString.Builder.HTTP.Chunked ( , chunkedTransferTerminator ) where -import Control.Applicative (pure) -import Control.Monad (void) -import Foreign (Ptr, Word8, (.&.)) +import Control.Monad (void, when) +import Foreign (Ptr, Word8, Word32, (.&.)) import qualified Foreign as F -import GHC.Base (Int(..), uncheckedShiftRL#) -import GHC.Word (Word32(..)) +import Data.ByteString (ByteString) import qualified Data.ByteString as S import Data.ByteString.Builder (Builder) -import Data.ByteString.Builder.Internal (BufferRange(..), BuildSignal) +import Data.ByteString.Builder.Internal (BufferRange(..), BuildSignal, BuildStep) import qualified Data.ByteString.Builder.Internal as B import qualified Data.ByteString.Builder.Prim as P import qualified Data.ByteString.Builder.Prim.Internal as P @@ -30,7 +28,7 @@ import Data.ByteString.Char8 () -- For the IsString in writeCRLF :: Ptr Word8 -> IO (Ptr Word8) writeCRLF op = do P.runF (P.char8 P.>*< P.char8) ('\r', '\n') op - pure $! op `F.plusPtr` 2 + pure $ op `F.plusPtr` crlfLength {-# INLINE crlfBuilder #-} crlfBuilder :: Builder @@ -40,41 +38,53 @@ crlfBuilder = P.primFixed (P.char8 P.>*< P.char8) ('\r', '\n') -- Hex Encoding Infrastructure ------------------------------------------------------------------------------ -{-# INLINE shiftr_w32 #-} -shiftr_w32 :: Word32 -> Int -> Word32 -shiftr_w32 (W32# w) (I# i) = W32# (w `uncheckedShiftRL#` i) +-- | Pad the chunk size with leading zeros? +data Padding + = NoPadding + | PadTo !Int --- | @writeWord32Hex len w op@ writes the hex encoding of @w@ to @op@ and +{-# INLINE writeWord32Hex #-} +writeWord32Hex :: Padding -> Word32 -> Ptr Word8 -> IO (Ptr Word8) +writeWord32Hex NoPadding w op = writeWord32Hex' (word32HexLength w) w op +writeWord32Hex (PadTo len) w op = writeWord32Hex' len w op + +-- | @writeWord32Hex' len w op@ writes the hex encoding of @w@ to @op@ and -- returns @op `'F.plusPtr'` len@. -- -- If writing @w@ doesn't consume all @len@ bytes, leading zeros are added. -{-# INLINE writeWord32Hex #-} -writeWord32Hex :: Int -> Word32 -> Ptr Word8 -> IO (Ptr Word8) -writeWord32Hex len w0 op0 = do +{-# INLINE writeWord32Hex' #-} +writeWord32Hex' :: Int -> Word32 -> Ptr Word8 -> IO (Ptr Word8) +writeWord32Hex' len w0 op0 = do go w0 (op0 `F.plusPtr` (len - 1)) - pure $! op0 `F.plusPtr` len + pure $ op0 `F.plusPtr` len where - go !w !op - | op < op0 = pure () - | otherwise = do + go !w !op = + when (op >= op0) $ do let nibble :: Word8 nibble = fromIntegral w .&. 0xF hex | nibble < 10 = 48 + nibble | otherwise = 55 + nibble F.poke op hex - go (w `shiftr_w32` 4) (op `F.plusPtr` (-1)) - -{-# INLINE iterationsUntilZero #-} -iterationsUntilZero :: Integral a => (a -> a) -> a -> Int -iterationsUntilZero f = go 0 - where - go !count 0 = count - go !count !x = go (count+1) (f x) + go (w `F.unsafeShiftR` 4) (op `F.plusPtr` (-1)) -- | Length of the hex-string required to encode the given 'Word32'. {-# INLINE word32HexLength #-} word32HexLength :: Word32 -> Int -word32HexLength = max 1 . iterationsUntilZero (`shiftr_w32` 4) +word32HexLength w = maxW32HexLength - (F.countLeadingZeros w `F.unsafeShiftR` 2) + +------------------------------------------------------------------------------ +-- Constants +------------------------------------------------------------------------------ + +crlfLength, maxW32HexLength, minimalChunkSize, maxBeforeBufferOverhead, + maxAfterBufferOverhead, maxEncodingOverhead, minimalBufferSize :: Int +crlfLength = 2 +maxW32HexLength = 8 -- 4 bytes, 2 hex digits per byte +minimalChunkSize = 1 +maxBeforeBufferOverhead = maxW32HexLength + crlfLength +maxAfterBufferOverhead = crlfLength + maxW32HexLength + crlfLength +maxEncodingOverhead = maxBeforeBufferOverhead + maxAfterBufferOverhead +minimalBufferSize = minimalChunkSize + maxEncodingOverhead ------------------------------------------------------------------------------ -- Chunked transfer encoding @@ -100,86 +110,73 @@ chunkedTransferEncoding :: Builder -> Builder chunkedTransferEncoding innerBuilder = B.builder transferEncodingStep where + transferEncodingStep :: forall a. BuildStep a -> BuildStep a transferEncodingStep k = go (B.runBuilder innerBuilder) where + go :: (BufferRange -> IO (BuildSignal _x)) -> BuildStep a go innerStep (BufferRange op ope) -- FIXME: Assert that outRemaining < maxBound :: Word32 | outRemaining < minimalBufferSize = pure $ B.bufferFull minimalBufferSize op (go innerStep) - | otherwise = do - let !brInner@(BufferRange opInner _) = BufferRange - (op `F.plusPtr` (maxChunkSizeLength + 2)) -- leave space for chunk header - (ope `F.plusPtr` (-maxAfterBufferOverhead)) -- leave space at end of data - - -- wraps the chunk, if it is non-empty, and returns the - -- signal constructed with the correct end-of-data pointer - {-# INLINE wrapChunk #-} - wrapChunk :: Ptr Word8 -> (Ptr Word8 -> IO (BuildSignal a)) - -> IO (BuildSignal a) - wrapChunk !chunkDataEnd mkSignal - | chunkDataEnd == opInner = mkSignal op - | otherwise = do - let chunkSize = fromIntegral $ chunkDataEnd `F.minusPtr` opInner - -- If the hex of chunkSize requires less space than - -- maxChunkSizeLength, we get leading zeros. - void $ writeWord32Hex maxChunkSizeLength chunkSize op - void $ writeCRLF (opInner `F.plusPtr` (-2)) - void $ writeCRLF chunkDataEnd - mkSignal (chunkDataEnd `F.plusPtr` 2) - - doneH opInner' _ = wrapChunk opInner' $ \op' -> do - let !br' = BufferRange op' ope - k br' - - fullH opInner' minRequiredSize nextInnerStep = - wrapChunk opInner' $ \op' -> - pure $! B.bufferFull - (minRequiredSize + maxEncodingOverhead) - op' - (go nextInnerStep) - - insertChunkH opInner' bs nextInnerStep - | S.null bs = -- flush - wrapChunk opInner' $ \op' -> - pure $! B.insertChunk op' S.empty (go nextInnerStep) - - | otherwise = -- insert non-empty bytestring - wrapChunk opInner' $ \op' -> do - -- add header for inserted bytestring - -- FIXME: assert(S.length bs < maxBound :: Word32) - let chunkSize = fromIntegral $ S.length bs - hexLength = word32HexLength chunkSize - !op'' <- writeWord32Hex hexLength chunkSize op' - !op''' <- writeCRLF op'' - - -- insert bytestring and write CRLF in next buildstep - pure $! B.insertChunk - op''' bs - (B.runBuilderWith crlfBuilder $ go nextInnerStep) - + | otherwise = -- execute inner builder with reduced boundaries B.fillWithBuildStep innerStep doneH fullH insertChunkH brInner where - -- minimal size guaranteed for actual data no need to require more - -- than 1 byte to guarantee progress the larger sizes will be - -- hopefully provided by the driver or requested by the wrapped - -- builders. - minimalChunkSize = 1 - - -- overhead computation - maxBeforeBufferOverhead = F.sizeOf (undefined :: Int) + 2 -- max chunk size and CRLF after header - maxAfterBufferOverhead = 2 + -- CRLF after data - F.sizeOf (undefined :: Int) + 2 -- max bytestring size, CRLF after header - - maxEncodingOverhead = maxBeforeBufferOverhead + maxAfterBufferOverhead - - minimalBufferSize = minimalChunkSize + maxEncodingOverhead - - -- remaining and required space computation outRemaining = ope `F.minusPtr` op maxChunkSizeLength = word32HexLength $ fromIntegral outRemaining --- | The zero-length chunk @0\\r\\n\\r\\n@ signaling the termination of the data transfer. + !brInner@(BufferRange opInner _) = BufferRange + (op `F.plusPtr` (maxChunkSizeLength + crlfLength)) -- leave space for chunk header + (ope `F.plusPtr` (-maxAfterBufferOverhead)) -- leave space at end of data + + doneH :: Ptr Word8 -> _x + -> IO (BuildSignal a) + doneH opInner' _ = + wrapChunk opInner' $ \op' -> + k $! BufferRange op' ope + + fullH :: Ptr Word8 -> Int -> BuildStep _x + -> IO (BuildSignal a) + fullH opInner' minRequiredSize nextInnerStep = + wrapChunk opInner' $ \op' -> + pure $! B.bufferFull + (minRequiredSize + maxEncodingOverhead) + op' + (go nextInnerStep) + + insertChunkH :: Ptr Word8 -> ByteString -> BuildStep _x + -> IO (BuildSignal a) + insertChunkH opInner' bs nextInnerStep = + wrapChunk opInner' $ \op' -> + if S.null bs -- flush + then pure $! B.insertChunk op' S.empty (go nextInnerStep) + else do -- insert non-empty bytestring + -- add header for inserted bytestring + -- FIXME: assert(S.length bs < maxBound :: Word32) + let chunkSize = fromIntegral $ S.length bs + !op'' <- writeWord32Hex NoPadding chunkSize op' + !op''' <- writeCRLF op'' + -- insert bytestring and write CRLF in next buildstep + pure $! B.insertChunk + op''' bs + (B.runBuilderWith crlfBuilder $ go nextInnerStep) + + -- wraps the chunk, if it is non-empty, and returns the + -- signal constructed with the correct end-of-data pointer + {-# INLINE wrapChunk #-} + wrapChunk :: Ptr Word8 -> (Ptr Word8 -> IO (BuildSignal a)) + -> IO (BuildSignal a) + wrapChunk !chunkDataEnd mkSignal + | chunkDataEnd == opInner = mkSignal op + | otherwise = do + let chunkSize = fromIntegral $ chunkDataEnd `F.minusPtr` opInner + void $ writeWord32Hex (PadTo maxChunkSizeLength) chunkSize op + void $ writeCRLF (opInner `F.plusPtr` (-crlfLength)) + void $ writeCRLF chunkDataEnd + mkSignal (chunkDataEnd `F.plusPtr` crlfLength) + + +-- | The zero-length chunk @0\\r\\n\\r\\n@ signalling the termination of the data transfer. chunkedTransferTerminator :: Builder chunkedTransferTerminator = B.byteStringCopy "0\r\n\r\n" diff --git a/bsb-http-chunked.cabal b/bsb-http-chunked.cabal index 956b530..a1a87da 100644 --- a/bsb-http-chunked.cabal +++ b/bsb-http-chunked.cabal @@ -1,5 +1,5 @@ Name: bsb-http-chunked -Version: 0.0.0.3 +Version: 0.0.0.4 Synopsis: Chunked HTTP transfer encoding for bytestring builders Description: This library contains functions for encoding [bytestring @@ -36,9 +36,8 @@ Source-repository head Library exposed-modules: Data.ByteString.Builder.HTTP.Chunked - build-depends: base >= 4.3 && < 4.13, - bytestring >= 0.9 && < 0.11, - bytestring-builder < 0.11 + build-depends: base >= 4.8 && < 4.13, + bytestring >= 0.10.2 && < 0.11 ghc-options: -Wall -O2 if impl(ghc >= 8.0) ghc-options: -Wcompat @@ -51,14 +50,11 @@ test-suite tests , bsb-http-chunked , blaze-builder >= 0.2.1.4 , bytestring - , bytestring-builder , hedgehog , tasty , tasty-hedgehog , tasty-hunit ghc-options: -Wall -rtsopts - if impl(ghc < 7.10) - buildable: False type: exitcode-stdio-1.0 test-suite doctests @@ -80,6 +76,4 @@ benchmark bench , gauge , semigroups ghc-options: -O2 -Wall -rtsopts - if impl(ghc < 7.10) - buildable: False type: exitcode-stdio-1.0 diff --git a/runbench.sh b/runbench.sh index c7efc47..92f929d 100755 --- a/runbench.sh +++ b/runbench.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -/opt/ghc/bin/cabal new-build --enable-benchmarks -O2 +/opt/ghc/bin/cabal new-build --enable-benchmarks -O2 -w ghc-8.4.3 -/home/simon/src/bsb-http-chunked/dist-newstyle/build/x86_64-linux/ghc-8.4.1/bsb-http-chunked-0.0.0.2/b/bench/opt/build/bench/bench --measure-with /home/simon/src/bsb-http-chunked/dist-newstyle/build/x86_64-linux/ghc-8.4.1/bsb-http-chunked-0.0.0.2/b/bench/opt/build/bench/bench --small +/home/simon/src/bsb-http-chunked/dist-newstyle/build/x86_64-linux/ghc-8.4.3/bsb-http-chunked-0.0.0.3/b/bench/opt/build/bench/bench --measure-with /home/simon/src/bsb-http-chunked/dist-newstyle/build/x86_64-linux/ghc-8.4.3/bsb-http-chunked-0.0.0.3/b/bench/opt/build/bench/bench --small