I have been working on a Bencoded string parser in order to improve my knowledge of Haskell. After running the code through hlint, I still have a few questions:
As I noted in the comments, the key value in the definition of BMapT should always be a Bstr. Is there any way to enforce a particular data constructor when pattern matching an algrebraic datatype?
Since Bencoded dictionaries are generally stored as human readable text, I made an instance of the Show typeclass so that I could take Bencode structures and turn them into text. I was wondering if this is the right thing to do, or if Haskell has another typeclass for storage/serialization a la Python's
repr
.This question is kind of nebulous, but would you consider this code 'idiomatic'? Code without style isn't worth writing :)
Code:
-- This is an implementation of Bencoding for bittorrent as described at
-- http://www.bittorrent.org/beps/bep_0003.html
module Bencode where
import Text.Parsec.ByteString
import Text.Parsec.Char
import Text.Parsec.Prim
import Text.Parsec.Combinator
import qualified Text.Parsec.Error as PE
import Data.Char
import qualified Data.Map as M
import qualified Control.Monad as Mon
-- Technically this first argument can only be a Bstr, but I don't know
-- how to express that
type BMapT = M.Map Bencode Bencode
-- I have no idea why Bstr is okay as a String when it's a ByteString
data Bencode = Bint Integer
| Bstr String
| Blist [Bencode]
| Bmap BMapT
deriving (Eq, Ord)
instance Show Bencode where
show (Bint i) = "i" ++ show i ++ "e"
show (Bstr s) = (show . length) s ++ ":" ++ s
show (Blist bs) = 'l':concatMap show bs ++ "e"
show (Bmap bm) = M.foldlWithKey (\a k b -> a ++ show k ++ show b) "d" bm ++ "e"
-- Parse a Bencoded Integer
bInt :: Parser Bencode
bInt = do char 'i'
num <- validNum
char 'e'
return $ Bint num
-- This parser parses valid integers in Bencodings
where validNum = do neg <- option ' ' (char '-')
d <- digit
case digitToInt d of
-- Only time the first digit == 0 is "i0e"
0 -> if neg == ' ' then
-- "i0e" allowed but NOT "i-0e" or zero padded integer
lookAhead (char 'e') >> return 0
else
parserFail "Can't have a negative zero"
_ -> many digit >>= \xs -> return $ read (neg:d:xs)
-- Parse a Bencoded String
bString :: Parser Bencode
bString = do ss <- many1 digit
char ':'
let size = read ss
Mon.liftM Bstr $ count size anyChar
bList :: Parser Bencode
bList = do char 'l'
ls <- many (bInt <|> bString <|> bList <|> bMap)
char 'e'
return $ Blist ls
-- A parser which parses bencoded dictionaries
bMap :: Parser Bencode
bMap = do char 'd'
entries <- many dictEntry
char 'e'
return $ Bmap $ M.fromList entries
-- This parser will parse a key-value pair
dictEntry :: Parser (Bencode, Bencode)
dictEntry = do key <- bString
value <- bString <|> bList <|> bInt <|> bMap
return (key, value)
-- This function reads a torrent file. readBencodedFile "filename" reads
-- that filename and returns the parsed bencoded dictionary
readBencodedFile :: String -> IO (Either PE.ParseError Bencode)
readBencodedFile = parseFromFile bMap