{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE BangPatterns #-}

module Network.Wai.Handler.Warp.File (
    RspFileInfo(..)
  , conditionalRequest
  , addContentHeadersForFilePart
  , H.parseByteRanges
  ) where

import Data.Array ((!))
import qualified Data.ByteString.Char8 as C8 (pack)
import Network.HTTP.Date
import qualified Network.HTTP.Types as H
import qualified Network.HTTP.Types.Header as H
import Network.Wai

import qualified Network.Wai.Handler.Warp.FileInfoCache as I
import Network.Wai.Handler.Warp.Header
import Network.Wai.Handler.Warp.Imports
import Network.Wai.Handler.Warp.PackInt


-- $setup
-- >>> import Test.QuickCheck

----------------------------------------------------------------

data RspFileInfo = WithoutBody H.Status
                 | WithBody H.Status H.ResponseHeaders Integer Integer
                 deriving (RspFileInfo -> RspFileInfo -> Bool
(RspFileInfo -> RspFileInfo -> Bool)
-> (RspFileInfo -> RspFileInfo -> Bool) -> Eq RspFileInfo
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
/= :: RspFileInfo -> RspFileInfo -> Bool
$c/= :: RspFileInfo -> RspFileInfo -> Bool
== :: RspFileInfo -> RspFileInfo -> Bool
$c== :: RspFileInfo -> RspFileInfo -> Bool
Eq,Int -> RspFileInfo -> ShowS
[RspFileInfo] -> ShowS
RspFileInfo -> String
(Int -> RspFileInfo -> ShowS)
-> (RspFileInfo -> String)
-> ([RspFileInfo] -> ShowS)
-> Show RspFileInfo
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
showList :: [RspFileInfo] -> ShowS
$cshowList :: [RspFileInfo] -> ShowS
show :: RspFileInfo -> String
$cshow :: RspFileInfo -> String
showsPrec :: Int -> RspFileInfo -> ShowS
$cshowsPrec :: Int -> RspFileInfo -> ShowS
Show)

----------------------------------------------------------------

conditionalRequest :: I.FileInfo
                   -> H.ResponseHeaders
                   -> IndexedHeader -- ^ Response
                   -> IndexedHeader -- ^ Request
                   -> RspFileInfo
conditionalRequest :: FileInfo
-> ResponseHeaders -> IndexedHeader -> IndexedHeader -> RspFileInfo
conditionalRequest FileInfo
finfo ResponseHeaders
hs0 IndexedHeader
rspidx IndexedHeader
reqidx = case RspFileInfo
condition of
    nobody :: RspFileInfo
nobody@(WithoutBody Status
_) -> RspFileInfo
nobody
    WithBody Status
s ResponseHeaders
_ Integer
off Integer
len   -> let !hs1 :: ResponseHeaders
hs1 = ResponseHeaders -> Integer -> Integer -> Integer -> ResponseHeaders
addContentHeaders ResponseHeaders
hs0 Integer
off Integer
len Integer
size
                                  !hasLM :: Bool
hasLM = Maybe HeaderValue -> Bool
forall a. Maybe a -> Bool
isJust (Maybe HeaderValue -> Bool) -> Maybe HeaderValue -> Bool
forall a b. (a -> b) -> a -> b
$ IndexedHeader
rspidx IndexedHeader -> Int -> Maybe HeaderValue
forall i e. Ix i => Array i e -> i -> e
! ResponseHeaderIndex -> Int
forall a. Enum a => a -> Int
fromEnum ResponseHeaderIndex
ResLastModified
                                  !hs :: ResponseHeaders
hs = [ (HeaderName
H.hLastModified,HeaderValue
date) | Bool -> Bool
not Bool
hasLM ] ResponseHeaders -> ResponseHeaders -> ResponseHeaders
forall a. [a] -> [a] -> [a]
++ ResponseHeaders
hs1
                              in Status -> ResponseHeaders -> Integer -> Integer -> RspFileInfo
WithBody Status
s ResponseHeaders
hs Integer
off Integer
len
  where
    !mtime :: HTTPDate
mtime = FileInfo -> HTTPDate
I.fileInfoTime FileInfo
finfo
    !size :: Integer
size  = FileInfo -> Integer
I.fileInfoSize FileInfo
finfo
    !date :: HeaderValue
date  = FileInfo -> HeaderValue
I.fileInfoDate FileInfo
finfo
    !mcondition :: Maybe RspFileInfo
mcondition = IndexedHeader -> Integer -> HTTPDate -> Maybe RspFileInfo
ifmodified    IndexedHeader
reqidx Integer
size HTTPDate
mtime
              Maybe RspFileInfo -> Maybe RspFileInfo -> Maybe RspFileInfo
forall (f :: * -> *) a. Alternative f => f a -> f a -> f a
<|> IndexedHeader -> Integer -> HTTPDate -> Maybe RspFileInfo
ifunmodified  IndexedHeader
reqidx Integer
size HTTPDate
mtime
              Maybe RspFileInfo -> Maybe RspFileInfo -> Maybe RspFileInfo
forall (f :: * -> *) a. Alternative f => f a -> f a -> f a
<|> IndexedHeader -> Integer -> HTTPDate -> Maybe RspFileInfo
ifrange       IndexedHeader
reqidx Integer
size HTTPDate
mtime
    !condition :: RspFileInfo
condition = RspFileInfo -> Maybe RspFileInfo -> RspFileInfo
forall a. a -> Maybe a -> a
fromMaybe (IndexedHeader -> Integer -> RspFileInfo
unconditional IndexedHeader
reqidx Integer
size) Maybe RspFileInfo
mcondition

----------------------------------------------------------------

ifModifiedSince :: IndexedHeader -> Maybe HTTPDate
ifModifiedSince :: IndexedHeader -> Maybe HTTPDate
ifModifiedSince IndexedHeader
reqidx = IndexedHeader
reqidx IndexedHeader -> Int -> Maybe HeaderValue
forall i e. Ix i => Array i e -> i -> e
! RequestHeaderIndex -> Int
forall a. Enum a => a -> Int
fromEnum RequestHeaderIndex
ReqIfModifiedSince Maybe HeaderValue
-> (HeaderValue -> Maybe HTTPDate) -> Maybe HTTPDate
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= HeaderValue -> Maybe HTTPDate
parseHTTPDate

ifUnmodifiedSince :: IndexedHeader -> Maybe HTTPDate
ifUnmodifiedSince :: IndexedHeader -> Maybe HTTPDate
ifUnmodifiedSince IndexedHeader
reqidx = IndexedHeader
reqidx IndexedHeader -> Int -> Maybe HeaderValue
forall i e. Ix i => Array i e -> i -> e
! RequestHeaderIndex -> Int
forall a. Enum a => a -> Int
fromEnum RequestHeaderIndex
ReqIfUnmodifiedSince Maybe HeaderValue
-> (HeaderValue -> Maybe HTTPDate) -> Maybe HTTPDate
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= HeaderValue -> Maybe HTTPDate
parseHTTPDate

ifRange :: IndexedHeader -> Maybe HTTPDate
ifRange :: IndexedHeader -> Maybe HTTPDate
ifRange IndexedHeader
reqidx = IndexedHeader
reqidx IndexedHeader -> Int -> Maybe HeaderValue
forall i e. Ix i => Array i e -> i -> e
! RequestHeaderIndex -> Int
forall a. Enum a => a -> Int
fromEnum RequestHeaderIndex
ReqIfRange Maybe HeaderValue
-> (HeaderValue -> Maybe HTTPDate) -> Maybe HTTPDate
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= HeaderValue -> Maybe HTTPDate
parseHTTPDate

----------------------------------------------------------------

ifmodified :: IndexedHeader -> Integer -> HTTPDate -> Maybe RspFileInfo
ifmodified :: IndexedHeader -> Integer -> HTTPDate -> Maybe RspFileInfo
ifmodified IndexedHeader
reqidx Integer
size HTTPDate
mtime = do
    HTTPDate
date <- IndexedHeader -> Maybe HTTPDate
ifModifiedSince IndexedHeader
reqidx
    RspFileInfo -> Maybe RspFileInfo
forall (m :: * -> *) a. Monad m => a -> m a
return (RspFileInfo -> Maybe RspFileInfo)
-> RspFileInfo -> Maybe RspFileInfo
forall a b. (a -> b) -> a -> b
$ if HTTPDate
date HTTPDate -> HTTPDate -> Bool
forall a. Eq a => a -> a -> Bool
/= HTTPDate
mtime
             then IndexedHeader -> Integer -> RspFileInfo
unconditional IndexedHeader
reqidx Integer
size
             else Status -> RspFileInfo
WithoutBody Status
H.notModified304

ifunmodified :: IndexedHeader -> Integer -> HTTPDate -> Maybe RspFileInfo
ifunmodified :: IndexedHeader -> Integer -> HTTPDate -> Maybe RspFileInfo
ifunmodified IndexedHeader
reqidx Integer
size HTTPDate
mtime = do
    HTTPDate
date <- IndexedHeader -> Maybe HTTPDate
ifUnmodifiedSince IndexedHeader
reqidx
    RspFileInfo -> Maybe RspFileInfo
forall (m :: * -> *) a. Monad m => a -> m a
return (RspFileInfo -> Maybe RspFileInfo)
-> RspFileInfo -> Maybe RspFileInfo
forall a b. (a -> b) -> a -> b
$ if HTTPDate
date HTTPDate -> HTTPDate -> Bool
forall a. Eq a => a -> a -> Bool
== HTTPDate
mtime
             then IndexedHeader -> Integer -> RspFileInfo
unconditional IndexedHeader
reqidx Integer
size
             else Status -> RspFileInfo
WithoutBody Status
H.preconditionFailed412

ifrange :: IndexedHeader -> Integer -> HTTPDate -> Maybe RspFileInfo
ifrange :: IndexedHeader -> Integer -> HTTPDate -> Maybe RspFileInfo
ifrange IndexedHeader
reqidx Integer
size HTTPDate
mtime = do
    HTTPDate
date <- IndexedHeader -> Maybe HTTPDate
ifRange IndexedHeader
reqidx
    HeaderValue
rng  <- IndexedHeader
reqidx IndexedHeader -> Int -> Maybe HeaderValue
forall i e. Ix i => Array i e -> i -> e
! RequestHeaderIndex -> Int
forall a. Enum a => a -> Int
fromEnum RequestHeaderIndex
ReqRange
    RspFileInfo -> Maybe RspFileInfo
forall (m :: * -> *) a. Monad m => a -> m a
return (RspFileInfo -> Maybe RspFileInfo)
-> RspFileInfo -> Maybe RspFileInfo
forall a b. (a -> b) -> a -> b
$ if HTTPDate
date HTTPDate -> HTTPDate -> Bool
forall a. Eq a => a -> a -> Bool
== HTTPDate
mtime
             then HeaderValue -> Integer -> RspFileInfo
parseRange HeaderValue
rng Integer
size
             else Status -> ResponseHeaders -> Integer -> Integer -> RspFileInfo
WithBody Status
H.ok200 [] Integer
0 Integer
size

unconditional :: IndexedHeader -> Integer -> RspFileInfo
unconditional :: IndexedHeader -> Integer -> RspFileInfo
unconditional IndexedHeader
reqidx Integer
size = case IndexedHeader
reqidx IndexedHeader -> Int -> Maybe HeaderValue
forall i e. Ix i => Array i e -> i -> e
! RequestHeaderIndex -> Int
forall a. Enum a => a -> Int
fromEnum RequestHeaderIndex
ReqRange of
    Maybe HeaderValue
Nothing  -> Status -> ResponseHeaders -> Integer -> Integer -> RspFileInfo
WithBody Status
H.ok200 [] Integer
0 Integer
size
    Just HeaderValue
rng -> HeaderValue -> Integer -> RspFileInfo
parseRange HeaderValue
rng Integer
size

----------------------------------------------------------------

parseRange :: ByteString -> Integer -> RspFileInfo
parseRange :: HeaderValue -> Integer -> RspFileInfo
parseRange HeaderValue
rng Integer
size = case HeaderValue -> Maybe ByteRanges
H.parseByteRanges HeaderValue
rng of
    Maybe ByteRanges
Nothing    -> Status -> RspFileInfo
WithoutBody Status
H.requestedRangeNotSatisfiable416
    Just []    -> Status -> RspFileInfo
WithoutBody Status
H.requestedRangeNotSatisfiable416
    Just (ByteRange
r:ByteRanges
_) -> let (!Integer
beg, !Integer
end) = ByteRange -> Integer -> (Integer, Integer)
checkRange ByteRange
r Integer
size
                      !len :: Integer
len = Integer
end Integer -> Integer -> Integer
forall a. Num a => a -> a -> a
- Integer
beg Integer -> Integer -> Integer
forall a. Num a => a -> a -> a
+ Integer
1
                      s :: Status
s = if Integer
beg Integer -> Integer -> Bool
forall a. Eq a => a -> a -> Bool
== Integer
0 Bool -> Bool -> Bool
&& Integer
end Integer -> Integer -> Bool
forall a. Eq a => a -> a -> Bool
== Integer
size Integer -> Integer -> Integer
forall a. Num a => a -> a -> a
- Integer
1 then
                              Status
H.ok200
                            else
                              Status
H.partialContent206
                  in Status -> ResponseHeaders -> Integer -> Integer -> RspFileInfo
WithBody Status
s [] Integer
beg Integer
len

checkRange :: H.ByteRange -> Integer -> (Integer, Integer)
checkRange :: ByteRange -> Integer -> (Integer, Integer)
checkRange (H.ByteRangeFrom   Integer
beg)     Integer
size = (Integer
beg, Integer
size Integer -> Integer -> Integer
forall a. Num a => a -> a -> a
- Integer
1)
checkRange (H.ByteRangeFromTo Integer
beg Integer
end) Integer
size = (Integer
beg,  Integer -> Integer -> Integer
forall a. Ord a => a -> a -> a
min (Integer
size Integer -> Integer -> Integer
forall a. Num a => a -> a -> a
- Integer
1) Integer
end)
checkRange (H.ByteRangeSuffix Integer
count)   Integer
size = (Integer -> Integer -> Integer
forall a. Ord a => a -> a -> a
max Integer
0 (Integer
size Integer -> Integer -> Integer
forall a. Num a => a -> a -> a
- Integer
count), Integer
size Integer -> Integer -> Integer
forall a. Num a => a -> a -> a
- Integer
1)

----------------------------------------------------------------

-- | @contentRangeHeader beg end total@ constructs a Content-Range 'H.Header'
-- for the range specified.
contentRangeHeader :: Integer -> Integer -> Integer -> H.Header
contentRangeHeader :: Integer -> Integer -> Integer -> Header
contentRangeHeader Integer
beg Integer
end Integer
total = (HeaderName
H.hContentRange, HeaderValue
range)
  where
    range :: HeaderValue
range = String -> HeaderValue
C8.pack
      -- building with ShowS
      (String -> HeaderValue) -> String -> HeaderValue
forall a b. (a -> b) -> a -> b
$ Char
'b' Char -> ShowS
forall a. a -> [a] -> [a]
: Char
'y'Char -> ShowS
forall a. a -> [a] -> [a]
: Char
't' Char -> ShowS
forall a. a -> [a] -> [a]
: Char
'e' Char -> ShowS
forall a. a -> [a] -> [a]
: Char
's' Char -> ShowS
forall a. a -> [a] -> [a]
: Char
' '
      Char -> ShowS
forall a. a -> [a] -> [a]
: (if Integer
beg Integer -> Integer -> Bool
forall a. Ord a => a -> a -> Bool
> Integer
end then (Char
'*'Char -> ShowS
forall a. a -> [a] -> [a]
:) else
          Integer -> ShowS
forall a. Integral a => a -> ShowS
showInt Integer
beg
          ShowS -> ShowS -> ShowS
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Char
'-' Char -> ShowS
forall a. a -> [a] -> [a]
:)
          ShowS -> ShowS -> ShowS
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Integer -> ShowS
forall a. Integral a => a -> ShowS
showInt Integer
end)
      ( Char
'/'
      Char -> ShowS
forall a. a -> [a] -> [a]
: Integer -> ShowS
forall a. Integral a => a -> ShowS
showInt Integer
total String
"")

addContentHeaders :: H.ResponseHeaders -> Integer -> Integer -> Integer -> H.ResponseHeaders
addContentHeaders :: ResponseHeaders -> Integer -> Integer -> Integer -> ResponseHeaders
addContentHeaders ResponseHeaders
hs Integer
off Integer
len Integer
size
  | Integer
len Integer -> Integer -> Bool
forall a. Eq a => a -> a -> Bool
== Integer
size = ResponseHeaders
hs'
  | Bool
otherwise   = let !ctrng :: Header
ctrng = Integer -> Integer -> Integer -> Header
contentRangeHeader Integer
off (Integer
off Integer -> Integer -> Integer
forall a. Num a => a -> a -> a
+ Integer
len Integer -> Integer -> Integer
forall a. Num a => a -> a -> a
- Integer
1) Integer
size
                  in Header
ctrngHeader -> ResponseHeaders -> ResponseHeaders
forall a. a -> [a] -> [a]
:ResponseHeaders
hs'
  where
    !lengthBS :: HeaderValue
lengthBS = Integer -> HeaderValue
forall a. Integral a => a -> HeaderValue
packIntegral Integer
len
    !hs' :: ResponseHeaders
hs' = (HeaderName
H.hContentLength, HeaderValue
lengthBS) Header -> ResponseHeaders -> ResponseHeaders
forall a. a -> [a] -> [a]
: (HeaderName
H.hAcceptRanges,HeaderValue
"bytes") Header -> ResponseHeaders -> ResponseHeaders
forall a. a -> [a] -> [a]
: ResponseHeaders
hs

-- |
--
-- >>> addContentHeadersForFilePart [] (FilePart 2 10 16)
-- [("Content-Range","bytes 2-11/16"),("Content-Length","10"),("Accept-Ranges","bytes")]
-- >>> addContentHeadersForFilePart [] (FilePart 0 16 16)
-- [("Content-Length","16"),("Accept-Ranges","bytes")]
addContentHeadersForFilePart :: H.ResponseHeaders -> FilePart -> H.ResponseHeaders
addContentHeadersForFilePart :: ResponseHeaders -> FilePart -> ResponseHeaders
addContentHeadersForFilePart ResponseHeaders
hs FilePart
part = ResponseHeaders -> Integer -> Integer -> Integer -> ResponseHeaders
addContentHeaders ResponseHeaders
hs Integer
off Integer
len Integer
size
  where
    off :: Integer
off = FilePart -> Integer
filePartOffset FilePart
part
    len :: Integer
len = FilePart -> Integer
filePartByteCount FilePart
part
    size :: Integer
size = FilePart -> Integer
filePartFileSize FilePart
part