2 - Copyright (C) 2015-2016 Ramakrishnan Muthukrishnan <ram@rkrishnan.org>
4 - This file is part of FuncTorrent.
6 - FuncTorrent is free software; you can redistribute it and/or modify
7 - it under the terms of the GNU General Public License as published by
8 - the Free Software Foundation; either version 3 of the License, or
9 - (at your option) any later version.
11 - FuncTorrent is distributed in the hope that it will be useful,
12 - but WITHOUT ANY WARRANTY; without even the implied warranty of
13 - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 - GNU General Public License for more details.
16 - You should have received a copy of the GNU General Public License
17 - along with FuncTorrent; if not, see <http://www.gnu.org/licenses/>
20 {-# LANGUAGE OverloadedStrings #-}
21 module FuncTorrent.Bencode
32 import Prelude hiding (length, concat)
34 import Data.ByteString (ByteString, length, concat)
35 import Data.ByteString.Char8 (unpack, pack)
36 import Data.Char (isLetter, isNumber, isPrint, isAscii)
37 import Data.Map.Strict (Map, fromList, toList)
38 import Text.ParserCombinators.Parsec
39 import qualified Text.Parsec.ByteString as ParsecBS
40 import Test.QuickCheck
42 data BVal = Bint Integer
45 | Bdict (Map String BVal)
46 deriving (Ord, Eq, Show)
48 genNonEmptyString :: Gen String
49 genNonEmptyString = arbitrary `suchThat` (\s -> ((s /= "") &&
53 instance Arbitrary ByteString where
54 arbitrary = pack <$> arbitrary
56 instance Arbitrary BVal where
57 arbitrary = sized bval
59 bval :: Int -> Gen BVal
60 bval 0 = oneof [ Bint <$> arbitrary
62 bval n = oneof [ Bint <$> arbitrary
64 , Blist <$> vectorOf n (bval (n `div` 4))
65 , do keys <- vectorOf n genNonEmptyString
66 vals <- vectorOf n (bval (n `div` 4))
67 return $ Bdict $ fromList $ zip keys vals ]
70 bValToInteger :: BVal -> Maybe Integer
71 bValToInteger (Bint x) = Just x
72 bValToInteger _ = Nothing
74 bValToBytestr :: BVal -> Maybe ByteString
75 bValToBytestr (Bstr bs) = Just bs
76 bValToBytestr _ = Nothing
78 bValToBList :: BVal -> Maybe [BVal]
79 bValToBList (Blist lst) = Just lst
80 bValToBList _ = Nothing
82 bValToInfoDict :: BVal -> Maybe (Map String BVal)
83 bValToInfoDict (Bdict dict) = Just dict
84 bValToInfoDict _ = Nothing
86 bstrToString :: BVal -> Maybe String
87 bstrToString bval = unpack <$> bValToBytestr bval
90 -- >>> import Data.Either
94 -- >>> parse bencStr "Bstr" (pack "4:spam")
96 -- >>> parse bencStr "Bstr" (pack "0:")
98 -- >>> parse bencStr "Bstr" (pack "0:hello")
101 bencStr :: ParsecBS.Parser ByteString
102 bencStr = do ds <- many1 digit <* char ':'
103 s <- count (read ds) anyChar
108 -- >>> parse bencInt "Bint" (pack "i42e")
110 -- >>> parse bencInt "Bint" (pack "i123e")
112 -- >>> parse bencInt "Bint" (pack "i1e")
114 -- >>> parse bencInt "Bint" (pack "i0e")
116 -- >>> parse bencInt "Bint" (pack "i-1e")
118 -- >>> isLeft $ parse bencInt "Bint" (pack "i01e")
120 -- >>> isLeft $ parse bencInt "Bint" (pack "i00e")
122 -- >>> isLeft $ parse bencInt "Bint" (pack "i002e")
124 bencInt :: ParsecBS.Parser Integer
125 bencInt = do ds <- between (char 'i') (char 'e') numbers
127 where numbers = do d' <- char '-' <|> digit
130 parseNumber '0' [] = return "0"
131 parseNumber '0' _ = unexpected "numbers cannot be left-padded with zeros"
132 parseNumber '-' [] = unexpected "sign without any digits"
133 parseNumber '-' (d'':_) | d'' == '0' = unexpected "numbers cannot be left-padded with zeros"
134 parseNumber d'' ds'' = return (d'':ds'')
138 -- >>> parse bencList "Blist" (pack "le")
140 -- >>> parse bencList "Blist" (pack "l4:spam4:eggse")
141 -- Right [Bstr "spam",Bstr "eggs"]
142 -- >>> parse bencList "Blist" (pack "l4:spami42ee")
143 -- Right [Bstr "spam",Bint 42]
144 -- >>> parse bencList "Blist" (pack "l4:spam4:eggsli42eee")
145 -- Right [Bstr "spam",Bstr "eggs",Blist [Bint 42]]
146 bencList :: ParsecBS.Parser [BVal]
147 bencList = between (char 'l') (char 'e') (many bencVal)
151 -- >>> parse bencDict "Bdict" (pack "de")
152 -- Right (fromList [])
153 -- >>> parse bencDict "Bdict" (pack "d3:cow3:moo4:spam4:eggse")
154 -- Right (fromList [("cow",Bstr "moo"),("spam",Bstr "eggs")])
155 -- >>> parse bencDict "Bdict" (pack "d4:spaml1:a1:bee")
156 -- Right (fromList [("spam",Blist [Bstr "a",Bstr "b"])])
157 -- >>> parse bencDict "Bdict" (pack "d9:publisher3:bob17:publisher-webpage15:www.example.com18:publisher.location4:homee")
158 -- Right (fromList [("publisher",Bstr "bob"),("publisher-webpage",Bstr "www.example.com"),("publisher.location",Bstr "home")])
159 bencDict :: ParsecBS.Parser (Map String BVal)
160 bencDict = between (char 'd') (char 'e') $ fromList <$> many kvpair
161 where kvpair = do k <- bdictKey
165 ds <- many1 digit <* char ':'
166 s <- count (read ds) alphaNum
170 bencVal :: ParsecBS.Parser BVal
171 bencVal = Bstr <$> bencStr <|>
173 Blist <$> bencList <|>
176 decode :: ByteString -> Either ParseError BVal
177 decode = parse bencVal "BVal"
179 -- Encode BVal into a bencoded ByteString. Inverse of decode
181 -- TODO: Use builders and lazy byte string to get O(1) concatenation over O(n)
182 -- provided by lists.
184 -- TODO: encode . decode pair might be a good candidate for Quickcheck.
185 -- | encode bencoded-values
187 -- >>> encode (Bstr (pack ""))
189 -- >>> encode (Bstr (pack "spam"))
191 -- >>> encode (Bint 0)
193 -- >>> encode (Bint 42)
195 -- >>> encode (Blist [(Bstr (pack "spam")), (Bstr (pack "eggs"))])
197 -- >>> encode (Blist [])
199 -- >>> encode (Bdict (fromList [("spam", Bstr $ pack "eggs")]))
201 encode :: BVal -> ByteString
202 encode (Bstr bs) = pack $ show (length bs) ++ ":" ++ unpack bs
203 encode (Bint i) = pack $ "i" ++ show i ++ "e"
204 encode (Blist xs) = concat ["l", concat $ map encode xs, "e"]
205 encode (Bdict d) = concat ["d", concat kvlist, "e"]
207 kvlist :: [ByteString]
208 kvlist = [encPair kv | kv <- toList d]
209 encPair (k, v) = concat [encode (Bstr (pack k)), encode v]