packages feed

torrent-10000.1.3: src/Data/Torrent.hs

-- | BitTorrent metainfo files
--
-- <http://www.bittorrent.org/beps/bep_0003.html>

{-# LANGUAGE DeriveDataTypeable #-}

module Data.Torrent
	( Torrent(..)
	, TorrentInfo(..)
	, TorrentFile(..)
	, readTorrent
	, serializeTorrent
	, torrentSize
	, showTorrent
	) where

import Data.BEncode
import Data.BEncode.Reader
import Data.Binary
import Data.Generics
import Data.Maybe
import Control.Applicative

import qualified Data.ByteString.Lazy as BS
import Data.ByteString.Lazy (ByteString)

import qualified Data.Map as Map

data Torrent
	= Torrent
		{ tAnnounce     :: Maybe ByteString
		, tAnnounceList :: [ByteString]
		, tComment      :: ByteString
		, tCreatedBy    :: Maybe ByteString
		, tInfo         :: TorrentInfo
		}
	deriving (Show, Read, Typeable, Data)

data TorrentInfo
	= SingleFile
		{ tLength      :: Integer
		, tName        :: ByteString
		, tPieceLength :: Integer
		, tPieces      :: ByteString }
	| MultiFile
		{ tFiles       :: [TorrentFile]
		, tName        :: ByteString
		, tPieceLength :: Integer
		, tPieces      :: ByteString
		}
	deriving (Show, Read, Typeable, Data)

data TorrentFile
	= TorrentFile
		{ fileLength :: Integer
		, 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

-- | Size of the files in the torrent.
torrentSize :: Torrent -> Integer
torrentSize torrent = case tInfo torrent of
	s@SingleFile{} -> tLength s
	MultiFile{tFiles=files} -> sum (map fileLength files)

readTorrent :: ByteString -> Either String Torrent
readTorrent inp = case bRead inp of
	Nothing -> Left "Not BEncoded"
	Just be -> runBReader parseTorrent be

parseTorrent :: BReader Torrent
parseTorrent = do
	announce <- optional $ dict "announce" bbytestring
	creator <- optional $ dict "created by" bbytestring
	dict "info" $ do
		name <- dict "name" bbytestring
		pLen <- dict "piece length" bint
		pieces <- dict "pieces" bbytestring
		torrentInfo <- parseTorrentInfo name pLen pieces
		return $ Torrent announce [] BS.empty creator torrentInfo

parseTorrentInfo :: ByteString -> Integer -> ByteString -> BReader TorrentInfo
parseTorrentInfo name pLen pieces = single <|> multi
  where
	single = do
		len <- dict "length" bint
		return $ SingleFile len name pLen pieces
	multi = do
		files <- dict "files" $ list $ do
			len <- dict "length" bint
			filePaths <- dict "path" (list bbytestring)
			return $ TorrentFile len filePaths
		return $ MultiFile files name pLen pieces

serializeTorrent :: Torrent -> BEncode
serializeTorrent torrent = BDict $ Map.fromList $ catMaybes
	[ fmap (\b -> ("announce", BString b)) (tAnnounce torrent)
	, Just ("comment", BString $ tComment torrent)
	, Just ("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 $ map serfile files) ]

	serfile file = BDict $ Map.fromList
		[ ("length", BInt (fileLength file))
		, ("path", BList (map BString $ filePath file))
		]

-- | generates a torrent file
--
-- Due to lexographical ordering requirements of BEncoded data, this
-- should generate the same ByteString that readTorrent read to generate
-- the Torrent. However, torrent files may contain extensions and
-- nonstandard fields that prevent that from holding for all torrent files.
showTorrent :: Torrent -> ByteString
showTorrent = bPack . serializeTorrent