packages feed

benc-0.1.1.0: src/Data/Bencode/AST.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE Strict #-}
{-# OPTIONS_GHC -fspec-constr #-}
module Data.Bencode.AST
  ( Value(..)
  , KeyValue(..)
  , parseOnly
  ) where

-- ATTENTION: This module is Strict!
--
-- Prefer to add definitions where laziness is desirable in another module
-- instead of here with a ~.
--
-- The core of this module has been inspected (with GHC 9.6.3 -O) to make sure
-- things are optimized and there is no unnecessary boxing to keep time and
-- allocations to a minimum. When modifying this file make sure to check the
-- core.

import Data.Char (isDigit)
import Data.List (intercalate)
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as BC
import qualified Data.Primitive.Array as A

import qualified Data.Bencode.Util as Util

-- | The Bencode AST.
data Value
  = String  {-# UNPACK #-} B.ByteString
  -- ^ Slice of the input @ByteString@.
  | Integer {-# UNPACK #-} B.ByteString
  -- ^ Slice of the input @ByteString@, containing a valid integer. Parsing
  -- into an integral type is done later if required.
  | List    {-# UNPACK #-} (A.Array Value)
  | Dict    {-# UNPACK #-} (A.Array KeyValue)
  deriving (Eq, Show)

-- | A Bencode dict's key-value pair.
data KeyValue = KeyValue
  {-# UNPACK #-} B.ByteString-- ^ Slice of the input @ByteString@.
  Value
  deriving (Eq, Show)

newtype Pos = Pos { unPos :: Int } deriving (Show, Num)

-- | Either an error or the parsed value together with the unparsed
-- section of the input and number of bytes parsed.
type ParseOneResult = Either String (Value, B.ByteString, Int)

data Stack
  = SNil
  | SList {-# UNPACK #-} Int [Value] Stack
  | SDict {-# UNPACK #-} B.ByteString {-# UNPACK #-} Int [KeyValue] Stack

-- | Parse one Bencode value from the given bytestring. Fails if the string is
-- not fully consumed.
parseOnly :: B.ByteString -> Either String Value
parseOnly s = case parseOne s of
  Left e -> Left e
  Right (v, s', n) ->
    if B.null s'
    then Right v
    else errorAtPos "ExpectedEOF" (Pos n)

-- | Parse one Bencode value from the given bytestring.
parseOne :: B.ByteString -> ParseOneResult
parseOne s = case BC.uncons s of
  Nothing -> errItem Nothing pos
  Just (c,s1) -> case c of
    _ | isDigit c ->
      parseString s pos $ \str s2 pos2 -> Right (String str, s2, unPos pos2)
    'i' -> do
      parseInteger s1 (pos+1) $ \i s2 pos2 -> Right (Integer i, s2, unPos pos2)
    'l' -> parseList SNil 0 [] s1 (pos+1)
    'd' -> parseDict SNil s1 (pos+1)
    _   -> errItem (Just c) pos
  where
    pos = Pos 0

-- | Parse a Bencode list. After the \'l\' marker.
parseList :: Stack -> Int -> [Value] -> B.ByteString -> Pos -> ParseOneResult
parseList stk n acc s pos = case BC.uncons s of
  Nothing -> errItemOrEnd Nothing pos
  Just (c,s1) -> case c of
    _ | isDigit c -> do
      parseString s pos $ \str -> parseList stk (n+1) (String str : acc)
    'i' -> do
      parseInteger s1 (pos+1) $ \i -> parseList stk (n+1) (Integer i : acc)
    'l' -> parseList (SList n acc stk) 0 [] s1 (pos+1)
    'd' -> parseDict (SList n acc stk) s1 (pos+1)
    'e' -> resumeParse stk (List (Util.arrayFromRevListN n acc)) s1 (pos+1)
    _   -> errItemOrEnd (Just c) pos

-- | Parse a Bencode dict. After the \'d\' marker.
parseDict :: Stack -> B.ByteString -> Pos -> ParseOneResult
parseDict stk s pos = case BC.uncons s of
  Nothing -> errStringOrEnd Nothing pos
  Just (c1,s1) -> case c1 of
    _ | isDigit c1 -> do
      parseString s pos $ \key s2 pos2 ->
        case BC.uncons s2 of
          Nothing -> errItem Nothing pos2
          Just (c3,s3) -> case c3 of
            _ | isDigit c3 -> do
              parseString s2 pos2 $ \str ->
                parseDict1 key stk 1 [KeyValue key (String str)]
            'i' -> do
              parseInteger s3 (pos2+1) $ \i ->
                parseDict1 key stk 1 [KeyValue key (Integer i)]
            'l' -> parseList (SDict key 0 [] stk) 0 [] s3 (pos2+1)
            'd' -> parseDict (SDict key 0 [] stk) s3 (pos2+1)
            _   -> errItem (Just c3) pos2
    'e' -> resumeParse stk (Dict (Util.arrayFromRevListN 0 [])) s1 (pos+1)
    _   -> errStringOrEnd (Just c1) pos

-- | Parse a Bencode dict. After the first key-value pair.
parseDict1 :: B.ByteString -> Stack -> Int -> [KeyValue] -> B.ByteString -> Pos
           -> ParseOneResult
parseDict1 pkey stk n acc s pos = case BC.uncons s of
  Nothing -> errStringOrEnd Nothing pos
  Just (c1,s1) -> case c1 of
    _ | isDigit c1 -> do
      parseString s pos $ \key s2 pos2 ->
        if pkey >= key
        then errUnsortedKeys pkey key pos
        else case BC.uncons s2 of
          Nothing -> errItem Nothing pos2
          Just (c3,s3) -> case c3 of
            _ | isDigit c3 -> do
              parseString s2 pos2 $ \str ->
                parseDict1 key stk (n+1) (KeyValue key (String str) : acc)
            'i' -> do
              parseInteger s3 (pos2+1) $ \i ->
                parseDict1 key stk (n+1) (KeyValue key (Integer i) : acc)
            'l' -> parseList (SDict key n acc stk) 0 [] s3 (pos2+1)
            'd' -> parseDict (SDict key n acc stk) s3 (pos2+1)
            _   -> errItem (Just c3) pos2
    'e' -> resumeParse stk (Dict (Util.arrayFromRevListN n acc)) s1 (pos+1)
    _   -> errStringOrEnd (Just c1) pos

-- | Add the value to the previously incomplete value on the stack, and resume
-- parsing it.
resumeParse :: Stack -> Value -> B.ByteString -> Pos -> ParseOneResult
resumeParse stk x s pos = case stk of
  SNil               -> Right (x, s, unPos pos)
  SList n xs stk1    -> parseList stk1 (n+1) (x:xs) s pos
  SDict k n acc stk1 -> parseDict1 k stk1 (n+1) (KeyValue k x : acc) s pos
{-# INLINE resumeParse #-}

-- | Parse a Bencode integer. After the \'i\' to the \'e\'.
parseInteger :: B.ByteString -> Pos
             -> (B.ByteString -> B.ByteString -> Pos -> ParseOneResult)
             -> ParseOneResult
parseInteger s pos k = case BC.uncons s of
  Nothing -> errDigit Nothing pos
  Just (c1,s1) -> case c1 of
    '0' -> end (B.take 1 s) s1 (pos+1)
    '-' -> case BC.span isDigit s1 of
      (x,s2) -> case BC.uncons x of
        Just (c3,_) | c3 /= '0' ->
          let n = B.length x + 1 in end (B.take n s) s2 (pos + Pos n)
        _ -> errNZDigit (fmap fst (BC.uncons s2)) (pos+1)
    _ -> case BC.span isDigit s of
      (x,s2) -> if B.null x
        then errDigitOrNeg (Just c1) pos
        else let n = B.length x in end x s2 (pos + Pos n)
  where
    end x s' pos' = case BC.uncons s' of
      Nothing -> errEnd Nothing pos'
      Just (c,s'') -> case c of
        'e' -> k x s'' (pos'+1)
        _   -> errEnd (Just c) pos'
    {-# INLINE end #-}
{-# INLINE parseInteger #-}

-- | Parse a Bencode string. From the length count to the end of the string.
parseString :: B.ByteString -> Pos
            -> (B.ByteString -> B.ByteString -> Pos -> ParseOneResult)
            -> ParseOneResult
parseString s pos k = case BC.span isDigit s of
  (digs,s1) ->
    case Util.readKnownNaturalAsInt False (BC.dropWhile (=='0') digs) of
      Nothing -> errTooLargeStringLength pos
      Just n ->
        let pos2 = pos + Pos (B.length digs)
        in case BC.uncons s1 of
             Nothing -> errColon Nothing pos2
             Just (c3,s3) -> case c3 of
               ':' -> case B.splitAt n s3 of
                 (str,!s4) | B.length str == n -> k str s4 (pos2 + 1 + Pos n)
                 -- For some reason GHC does not realize without the bang that
                 -- s4 does not need to be boxed.
                 _ -> errTooLargeStringLength pos
               _   -> errColon (Just c3) pos2
{-# INLINE parseString #-}

------------------------------
-- Error stuff
------------------------------

errorAtPos :: String -> Pos -> Either String a
errorAtPos e (Pos n) = Left $ "ParseErrorAt " ++ show n ++ ": " ++ e

mismatch :: [String] -> Maybe Char -> Pos -> Either String a
mismatch cs c = errorAtPos $
  "ExpectedOneOfButGot [" ++ intercalate "," cs ++ "] " ++ maybe "EOF" show c

errItem, errItemOrEnd, errStringOrEnd, errEnd, errDigit, errNZDigit,
  errColon, errDigitOrNeg :: Maybe Char -> Pos -> Either String a
errItem        = mismatch ["Digit", show 'i', show 'l', show 'd']
errItemOrEnd   = mismatch ["Digit", show 'i', show 'l', show 'd', show 'e']
errStringOrEnd = mismatch ["Digit", show 'e']
errEnd         = mismatch [show 'e']
errDigit       = mismatch ["Digit"]
errNZDigit     = mismatch ["NonZeroDigit"]
errDigitOrNeg  = mismatch ["Digit", show '-']
errColon       = mismatch [show ':']

errUnsortedKeys :: B.ByteString -> B.ByteString -> Pos -> Either String a
errUnsortedKeys pkey key = errorAtPos $
  "UnsortedKeys " ++ show pkey ++ " " ++ show key

errTooLargeStringLength :: Pos -> Either String a
errTooLargeStringLength = errorAtPos "TooLargeStringLength"