packages feed

http2-5.4.4: Network/HPACK/Table/Dynamic.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RecordWildCards #-}

module Network.HPACK.Table.Dynamic (
    DynamicTable (..),
    newDynamicTableForEncoding,
    newDynamicTableForDecoding,
    renewDynamicTable,
    huffmanDecoder,
    printDynamicTable,
    isDynamicTableEmpty,
    isSuitableSize,
    TableSizeAction (..),
    needChangeTableSize,
    setLimitForEncoding,
    resetLimitForEncoding,
    insertEntry,
    toDynamicEntry,
    CodeInfo (..),
    withDynamicTableForEncoding,
    withDynamicTableForDecoding,
    toIndexedEntry,
    fromHIndexToIndex,
    getRevIndex,
) where

import qualified Control.Exception as E
import Data.Array.Base (unsafeRead, unsafeWrite)
import Data.Array.IO (IOArray, newArray)
import qualified Data.ByteString.Char8 as BS
import Data.IORef

import Imports
import Network.HPACK.Huffman
import Network.HPACK.Table.Entry
import Network.HPACK.Table.RevIndex
import Network.HPACK.Table.Static
import Network.HPACK.Types

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

-- For decoder
{-# INLINE toIndexedEntry #-}
toIndexedEntry :: DynamicTable -> Index -> IO Entry
toIndexedEntry dyntbl idx
    | idx <= 0 = E.throwIO $ IndexOverrun idx
    | idx <= staticTableSize = return $ toStaticEntry idx
    | otherwise = toDynamicEntry dyntbl idx

-- For encoder
{-# INLINE fromHIndexToIndex #-}
fromHIndexToIndex :: DynamicTable -> HIndex -> IO Index
fromHIndexToIndex _ (SIndex idx) = return idx
fromHIndexToIndex DynamicTable{..} (DIndex didx) = do
    maxN <- readIORef maxNumOfEntries
    off <- readIORef offset
    x <- adj maxN (didx - off)
    return $ x + staticTableSize

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

type Table = IOArray Index Entry

{-
        offset
        v
   +-+-+-+-+-+-+-+-+
   | | | |z|y|x| | |
   +-+-+-+-+-+-+-+-+
          1 2 3      (numOfEntries = 3)

After insertion:

      offset
      v
   +-+-+-+-+-+-+-+-+
   | | |w|z|y|x| | |
   +-+-+-+-+-+-+-+-+
        1 2 3 4      (numOfEntries = 4)
-}

data CodeInfo = CIE EncodeInfo | CID DecodeInfo
data EncodeInfo
    = EncodeInfo
        RevIndex -- Reverse index
        -- The value informed by SETTINGS_HEADER_TABLE_SIZE.
        -- If 'Nothing', dynamic table size update is not necessary.
        -- Otherwise, dynamic table size update is sent
        -- and this value should be set to 'Nothing'.
        (IORef (Maybe Size))
data DecodeInfo
    = DecodeInfo
        HuffmanDecoder
        (IORef Size) -- The limit size

toEncodeInfo :: CodeInfo -> EncodeInfo
toEncodeInfo (CIE x) = x
toEncodeInfo _ = error "toEncodeInfo"

toDecodeInfo :: CodeInfo -> DecodeInfo
toDecodeInfo (CID x) = x
toDecodeInfo _ = error "toDecodeInfo"

-- | Type for dynamic table.
data DynamicTable = DynamicTable
    { codeInfo :: CodeInfo
    , circularTable :: IORef Table
    -- ^ An array
    , offset :: IORef Index
    -- ^ Start point
    , numOfEntries :: IORef Int
    -- ^ The current number of entries
    , maxNumOfEntries :: IORef Int
    -- ^ The size of the array
    , dynamicTableSize :: IORef Size
    -- ^ The current dynamic table size (defined in HPACK)
    , maxDynamicTableSize :: IORef Size
    -- ^ The max dynamic table size (defined in HPACK)
    }

{-# INLINE adj #-}
adj :: Int -> Int -> IO Int
adj maxN x
    | maxN == 0 = E.throwIO TooSmallTableSize
    | otherwise =
        let ret = (x + maxN) `mod` maxN
         in return ret

huffmanDecoder :: DynamicTable -> HuffmanDecoder
huffmanDecoder DynamicTable{..} = dec
  where
    DecodeInfo dec _ = toDecodeInfo codeInfo

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

-- | Printing 'DynamicTable'.
printDynamicTable :: DynamicTable -> IO ()
printDynamicTable DynamicTable{..} = do
    maxN <- readIORef maxNumOfEntries
    off <- readIORef offset
    n <- readIORef numOfEntries
    let beg = off + 1
        end = off + n
    tbl <- readIORef circularTable
    es <- mapM (adj maxN >=> unsafeRead tbl) [beg .. end]
    let ts = zip [1 ..] es
    mapM_ printEntry ts
    dsize <- readIORef dynamicTableSize
    maxdsize <- readIORef maxDynamicTableSize
    putStrLn $ "      Table size: " ++ show dsize ++ "/" ++ show maxdsize

printEntry :: (Index, Entry) -> IO ()
printEntry (i, e) = do
    putStr "[ "
    putStr $ show i
    putStr "] (s = "
    putStr $ show $ entrySize e
    putStr ") "
    BS.putStr $ original $ entryHeaderName e
    putStr ": "
    BS.putStrLn $ entryFieldValue e

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

isDynamicTableEmpty :: DynamicTable -> IO Bool
isDynamicTableEmpty DynamicTable{..} = do
    n <- readIORef numOfEntries
    return $ n == 0

isSuitableSize :: Size -> DynamicTable -> IO Bool
isSuitableSize siz DynamicTable{..} = do
    let DecodeInfo _ limref = toDecodeInfo codeInfo
    lim <- readIORef limref
    return $ siz <= lim

data TableSizeAction = Keep | Change Size | Ignore Size

needChangeTableSize :: DynamicTable -> IO TableSizeAction
needChangeTableSize DynamicTable{..} = do
    let EncodeInfo _ limref = toEncodeInfo codeInfo
    mlim <- readIORef limref
    maxsiz <- readIORef maxDynamicTableSize
    return $ case mlim of
        Nothing -> Keep
        Just lim
            | lim < maxsiz -> Change lim
            | otherwise -> Ignore maxsiz

-- | When SETTINGS_HEADER_TABLE_SIZE is received from a peer,
--   its value should be set by this function.
setLimitForEncoding :: Size -> DynamicTable -> IO ()
setLimitForEncoding siz DynamicTable{..} = do
    let EncodeInfo _ limref = toEncodeInfo codeInfo
    writeIORef limref $ Just siz

resetLimitForEncoding :: DynamicTable -> IO ()
resetLimitForEncoding DynamicTable{..} = do
    let EncodeInfo _ limref = toEncodeInfo codeInfo
    writeIORef limref Nothing

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

-- | Creating 'DynamicTable' for encoding.
newDynamicTableForEncoding
    :: Size
    -- ^ The dynamic table size
    -> IO DynamicTable
newDynamicTableForEncoding maxsiz = do
    rev <- newRevIndex
    lim <- newIORef Nothing
    let info = CIE $ EncodeInfo rev lim
    newDynamicTable maxsiz info

-- | Creating 'DynamicTable' for decoding.
newDynamicTableForDecoding
    :: Size
    -- ^ The dynamic table size
    -> Size
    -- ^ The size of temporary buffer for Huffman decoding
    -> IO DynamicTable
newDynamicTableForDecoding maxsiz huftmpsiz = do
    lim <- newIORef maxsiz
    buf <- mallocPlainForeignPtrBytes huftmpsiz
    let decoder = decodeH buf huftmpsiz
        info = CID $ DecodeInfo decoder lim
    newDynamicTable maxsiz info

newDynamicTable :: Size -> CodeInfo -> IO DynamicTable
newDynamicTable maxsiz info = do
    tbl <- newArray (0, end) dummyEntry
    DynamicTable info
        <$> newIORef tbl -- circularTable
        <*> newIORef end -- offset
        <*> newIORef 0 -- numOfEntries
        <*> newIORef maxN -- maxNumOfEntries
        <*> newIORef 0 -- dynamicTableSize
        <*> newIORef maxsiz -- maxDynamicTableSize
  where
    maxN = maxNumbers maxsiz
    end = maxN - 1

-- | Renewing 'DynamicTable' with necessary entries copied.
renewDynamicTable :: Size -> DynamicTable -> IO ()
renewDynamicTable maxsiz dyntbl@DynamicTable{..} = do
    renew <- shouldRenew dyntbl maxsiz
    when renew $ do
        entries <- getEntries dyntbl
        let maxN = maxNumbers maxsiz
            end = maxN - 1
        newtbl <- newArray (0, end) dummyEntry
        writeIORef circularTable newtbl
        writeIORef offset end
        writeIORef numOfEntries 0
        writeIORef maxNumOfEntries maxN
        writeIORef dynamicTableSize 0
        writeIORef maxDynamicTableSize maxsiz
        case codeInfo of
            CIE (EncodeInfo rev _) -> renewRevIndex rev
            _ -> return ()
        copyEntries dyntbl entries

getEntries :: DynamicTable -> IO [Entry]
getEntries DynamicTable{..} = do
    maxN <- readIORef maxNumOfEntries
    off <- readIORef offset
    n <- readIORef numOfEntries
    table <- readIORef circularTable
    let readTable i = adj maxN (off + i) >>= unsafeRead table
    forM [1 .. n] readTable

copyEntries :: DynamicTable -> [Entry] -> IO ()
copyEntries _ [] = return ()
copyEntries dyntbl@DynamicTable{..} (e : es) = do
    dsize <- readIORef dynamicTableSize
    maxdsize <- readIORef maxDynamicTableSize
    when (dsize + entrySize e <= maxdsize) $ do
        insertEnd e dyntbl
        copyEntries dyntbl es

-- | Is the size of 'DynamicTable' really changed?
shouldRenew :: DynamicTable -> Size -> IO Bool
shouldRenew DynamicTable{..} maxsiz = do
    maxdsize <- readIORef maxDynamicTableSize
    return $ maxdsize /= maxsiz

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

-- | Creating 'DynamicTable' for encoding,
--   performing the action and
--   clearing the 'DynamicTable'.
withDynamicTableForEncoding
    :: Size
    -- ^ The dynamic table size
    -> (DynamicTable -> IO a)
    -> IO a
withDynamicTableForEncoding maxsiz action =
    newDynamicTableForEncoding maxsiz >>= action

-- | Creating 'DynamicTable' for decoding,
--   performing the action and
--   clearing the 'DynamicTable'.
withDynamicTableForDecoding
    :: Size
    -- ^ The dynamic table size
    -> Size
    -- ^ The size of temporary buffer for Huffman
    -> (DynamicTable -> IO a)
    -> IO a
withDynamicTableForDecoding maxsiz huftmpsiz action =
    newDynamicTableForDecoding maxsiz huftmpsiz >>= action

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

-- | Inserting 'Entry' to 'DynamicTable'.
insertEntry :: Entry -> DynamicTable -> IO ()
insertEntry e dyntbl@DynamicTable{..} = do
    -- Theoretically speaking, dropping entries by adjustTableSize
    -- should be first. However, non-used slots always exist since the
    -- size of dynamic table calculated via the minimum entry size (32
    -- bytes). To simply adjustTableSize, insertFront is called first.
    insertFront e dyntbl
    es <- adjustTableSize dyntbl
    case codeInfo of
        CIE (EncodeInfo rev _) -> deleteRevIndexList es rev
        _ -> return ()

insertFront :: Entry -> DynamicTable -> IO ()
insertFront e DynamicTable{..} = do
    maxN <- readIORef maxNumOfEntries
    off <- readIORef offset
    n <- readIORef numOfEntries
    dsize <- readIORef dynamicTableSize
    table <- readIORef circularTable
    let i = off
        dsize' = dsize + entrySize e
    if maxN == 0
        then return ()
        else do
            off' <- adj maxN (off - 1)
            unsafeWrite table i e
            writeIORef offset off'
            writeIORef numOfEntries $ n + 1
            writeIORef dynamicTableSize dsize'
            case codeInfo of
                CIE (EncodeInfo rev _) -> insertRevIndex e (DIndex i) rev
                _ -> return ()

adjustTableSize :: DynamicTable -> IO [Entry]
adjustTableSize dyntbl@DynamicTable{..} = adjust []
  where
    adjust :: [Entry] -> IO [Entry]
    adjust es = do
        dsize <- readIORef dynamicTableSize
        maxdsize <- readIORef maxDynamicTableSize
        if dsize <= maxdsize
            then return es
            else do
                e <- removeEnd dyntbl
                adjust (e : es)

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

-- Used in copyEntries.
insertEnd :: Entry -> DynamicTable -> IO ()
insertEnd e DynamicTable{..} = do
    maxN <- readIORef maxNumOfEntries
    off <- readIORef offset
    n <- readIORef numOfEntries
    dsize <- readIORef dynamicTableSize
    table <- readIORef circularTable
    i <- adj maxN (off + n + 1)
    let dsize' = dsize + entrySize e
    unsafeWrite table i e
    writeIORef numOfEntries $ n + 1
    writeIORef dynamicTableSize dsize'
    case codeInfo of
        CIE (EncodeInfo rev _) -> insertRevIndex e (DIndex i) rev
        _ -> return ()

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

removeEnd :: DynamicTable -> IO Entry
removeEnd DynamicTable{..} = do
    maxN <- readIORef maxNumOfEntries
    off <- readIORef offset
    n <- readIORef numOfEntries
    i <- adj maxN (off + n)
    table <- readIORef circularTable
    e <- unsafeRead table i
    unsafeWrite table i dummyEntry -- let the entry GCed
    dsize <- readIORef dynamicTableSize
    let dsize' = dsize - entrySize e
    writeIORef numOfEntries (n - 1)
    writeIORef dynamicTableSize dsize'
    return e

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

{-# INLINE toDynamicEntry #-}
toDynamicEntry :: DynamicTable -> Index -> IO Entry
toDynamicEntry DynamicTable{..} idx = do
    maxN <- readIORef maxNumOfEntries
    off <- readIORef offset
    n <- readIORef numOfEntries
    when (idx > n + staticTableSize) $ E.throwIO $ IndexOverrun idx
    didx <- adj maxN (idx + off - staticTableSize)
    table <- readIORef circularTable
    unsafeRead table didx

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

{-# INLINE getRevIndex #-}
getRevIndex :: DynamicTable -> RevIndex
getRevIndex DynamicTable{..} = rev
  where
    EncodeInfo rev _ = toEncodeInfo codeInfo