torrent-2007.10.27: src/Data/Torrent.hs
{-# OPTIONS -fglasgow-exts #-}
module Data.Torrent
( Torrent(..)
, TorrentInfo(..)
, TorrentFile(..)
, readTorrent -- :: ByteString -> Either String Torrent
, serializeTorrent
, torrentSize -- :: Torrent -> Int
) where
-- import Network.URI
import Data.BEncode
import Data.BEncode.Parser
import Data.Torrent.SHA1
import Data.Maybe
import Data.Binary
import Data.Generics
import qualified Data.ByteString.Char8 as BS
import Data.ByteString (ByteString)
import qualified Data.Map as Map
data Torrent
= Torrent
{ tAnnounce :: ByteString
, tAnnounceList :: [ByteString]
, tComment :: ByteString
, tCreatedBy :: Maybe ByteString
, tInfo :: TorrentInfo
, tInfoHash :: ByteString
} deriving (Show, Read, Typeable, Data)
data TorrentInfo
= SingleFile
{ tLength :: Int
, tName :: ByteString
, tPieceLength :: Int
, tPieces :: ByteString }
| MultiFile
{ tFiles :: [TorrentFile]
, tName :: ByteString
, tPieceLength :: Int
, tPieces :: ByteString
} deriving (Show, Read, Typeable, Data)
data TorrentFile
= TorrentFile
{ fileLength :: Int
, filePath :: [ByteString]
} deriving (Show, Read, Typeable, Data)
instance Binary Torrent where
put = put . serializeTorrent
get = do e <- get
case readTorrent e of
Left err -> fail $ "Failed to parse torrent: " ++ err
Right t -> return t
torrentSize :: Torrent -> Int
torrentSize torrent
= case tInfo torrent of
s@SingleFile{} -> tLength s
MultiFile{tFiles=files} -> sum (map fileLength files)
{-
buri :: BParser BEncode -> BParser URI
buri p = do str <- bstring p
case parseURI str of
Nothing -> fail $ "Expected URI: " ++ str
Just uri -> return uri
-}
readTorrent :: ByteString -> Either String Torrent
readTorrent inp
= case bRead inp of
Nothing -> Left "Not BEncoded"
Just be -> runParser parseTorrent be
parseTorrent :: BParser Torrent
parseTorrent =
do announce <- bbytestring $ dict "announce"
creator <- optional $ bbytestring $ dict "created by"
info <- dict "info"
setInput info
name <- bbytestring $ dict "name"
pLen <- bint $ dict "piece length"
pieces <- bbytestring $ dict "pieces"
torrentInfo <- parseTorrentInfo name pLen pieces
let infoHash = sha1 (BS.pack $ bShow info "")
return $ Torrent announce [] BS.empty creator torrentInfo infoHash
parseTorrentInfo :: ByteString -> Int -> ByteString -> BParser TorrentInfo
parseTorrentInfo name pLen pieces
= do len <- bint $ dict "length"
return $ SingleFile len name pLen pieces
<|>
do files <- list "files" $ do len <- bint $ dict "length"
filePaths <- list "path" $ bbytestring token
return $ TorrentFile len filePaths
return $ MultiFile files name pLen pieces
serializeTorrent :: Torrent -> BEncode
serializeTorrent torrent
= BDict $ Map.fromList [("announce",BString $ tAnnounce torrent)
,("comment", BString $ tComment torrent)
,("info",info)
]
where info = BDict $ Map.fromList $ [("name", BString $ tName (tInfo torrent))
,("pieces", BString $ tPieces (tInfo torrent))
,("piece length", BInt $ tPieceLength (tInfo torrent))
] ++ case tInfo torrent of
SingleFile len _ _ _ -> [("length", BInt len)]
MultiFile files _ _ _ -> [("files", BList $ flip map files $ \file ->
BDict $ Map.fromList [("length", BInt (fileLength file))
,("path", BList (map BString $ filePath file))]
)]