summaryrefslogtreecommitdiff
path: root/src/Data
diff options
context:
space:
mode:
authorSam Truzjan <pxqr.sta@gmail.com>2013-10-31 11:25:59 +0400
committerSam Truzjan <pxqr.sta@gmail.com>2013-10-31 11:25:59 +0400
commit01cef3fafc27d39d88c94cacdcd8e204c5f66b86 (patch)
tree01040aca19e49f4e7937383fef53b8c82bcec12b /src/Data
parentc1fec260f47084300ac30de2e43d52966316a2c7 (diff)
Merge bittorrent package with torrent-content
Diffstat (limited to 'src/Data')
-rw-r--r--src/Data/Torrent.hs273
-rw-r--r--src/Data/Torrent/InfoHash.hs115
-rw-r--r--src/Data/Torrent/Layout.hs273
-rw-r--r--src/Data/Torrent/Magnet.hs236
-rw-r--r--src/Data/Torrent/Piece.hs203
-rw-r--r--src/Data/Torrent/Tree.hs71
6 files changed, 1171 insertions, 0 deletions
diff --git a/src/Data/Torrent.hs b/src/Data/Torrent.hs
new file mode 100644
index 00000000..15ada35f
--- /dev/null
+++ b/src/Data/Torrent.hs
@@ -0,0 +1,273 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- Torrent file contains metadata about files and folders but not
9-- content itself. The files are bencoded dictionaries. There is
10-- also other info which is used to help join the swarm.
11--
12-- This module provides torrent metainfo serialization and info hash
13-- extraction.
14--
15-- For more info see:
16-- <http://www.bittorrent.org/beps/bep_0003.html#metainfo-files>,
17-- <https://wiki.theory.org/BitTorrentSpecification#Metainfo_File_Structure>
18--
19{-# LANGUAGE CPP #-}
20{-# LANGUAGE FlexibleInstances #-}
21{-# LANGUAGE BangPatterns #-}
22{-# LANGUAGE GeneralizedNewtypeDeriving #-}
23{-# LANGUAGE DeriveDataTypeable #-}
24{-# LANGUAGE TemplateHaskell #-}
25{-# OPTIONS -fno-warn-orphans #-}
26-- TODO refine interface
27module Data.Torrent
28 ( -- * Info dictionary
29 InfoDict (..)
30 , infohash
31 , layoutInfo
32 , pieceInfo
33 , isPrivate
34
35 -- * Torrent file
36 , Torrent(..)
37 , announce
38 , announceList
39 , comment
40 , createdBy
41 , creationDate
42 , encoding
43 , infoDict
44 , publisher
45 , publisherURL
46 , signature
47
48 , nullTorrent
49
50 -- * IO
51 , torrentExt
52 , isTorrentPath
53 , fromFile
54 , toFile
55
56{-
57 , nullTorrent
58 , mktorrent
59
60
61-}
62 ) where
63
64import Prelude hiding (sum)
65
66import Control.Applicative
67import Control.DeepSeq
68import Control.Exception
69import Control.Lens
70
71import Data.Aeson.TH
72import Data.BEncode as BE
73import Data.BEncode.Types as BE
74import Data.ByteString as BS
75import qualified Data.ByteString.Char8 as BC (pack, unpack)
76import qualified Data.ByteString.Lazy as BL
77import Data.Char
78import Data.Hashable as Hashable
79import qualified Data.List as L
80import Data.Text (Text)
81import Data.Time.Clock.POSIX
82import Data.Typeable
83import Network.URI
84import System.FilePath
85
86import Data.Torrent.InfoHash as IH
87import Data.Torrent.Layout
88import Data.Torrent.Piece
89
90
91{-----------------------------------------------------------------------
92-- Info dictionary
93-----------------------------------------------------------------------}
94
95{- note that info hash is actually reduntant field
96 but it's better to keep it here to avoid heavy recomputations
97-}
98
99-- | Info part of the .torrent file contain info about each content file.
100data InfoDict = InfoDict
101 { idInfoHash :: !InfoHash
102 -- ^ SHA1 hash of the (other) 'DictInfo' fields.
103 , idLayoutInfo :: !LayoutInfo
104 , idPieceInfo :: !PieceInfo
105 , idPrivate :: !Bool
106 -- ^ If set the client MUST publish its presence to get other
107 -- peers ONLY via the trackers explicity described in the
108 -- metainfo file.
109 --
110 -- BEP 27: <http://www.bittorrent.org/beps/bep_0027.html>
111 } deriving (Show, Read, Eq, Typeable)
112
113$(deriveJSON (L.map toLower . L.dropWhile isLower) ''InfoDict)
114
115makeLensesFor
116 [ ("idInfoHash" , "infohash" )
117 , ("idLayoutInfo", "layoutInfo")
118 , ("idPieceInfo" , "pieceInfo" )
119 , ("idPrivate" , "isPrivate" )
120 ]
121 ''InfoDict
122
123instance NFData InfoDict where
124 rnf InfoDict {..} = rnf idLayoutInfo
125
126instance Hashable InfoDict where
127 hash = Hashable.hash . idInfoHash
128 {-# INLINE hash #-}
129
130getPrivate :: Get Bool
131getPrivate = (Just True ==) <$>? "private"
132
133putPrivate :: Bool -> BDict -> BDict
134putPrivate False = id
135putPrivate True = \ cont -> "private" .=! True .: cont
136
137instance BEncode InfoDict where
138 toBEncode InfoDict {..} = toDict $
139 putLayoutInfo idLayoutInfo $
140 putPieceInfo idPieceInfo $
141 putPrivate idPrivate $
142 endDict
143
144 fromBEncode dict = (`fromDict` dict) $ do
145 InfoDict ih <$> getLayoutInfo
146 <*> getPieceInfo
147 <*> getPrivate
148 where
149 ih = IH.hashlazy (encode dict)
150
151{-----------------------------------------------------------------------
152-- Torrent info
153-----------------------------------------------------------------------}
154
155-- | Metainfo about particular torrent.
156data Torrent = Torrent
157 { tAnnounce :: !URI
158 -- ^ The URL of the tracker.
159
160 , tAnnounceList :: !(Maybe [[URI]])
161 -- ^ Announce list add multiple tracker support.
162 --
163 -- BEP 12: <http://www.bittorrent.org/beps/bep_0012.html>
164
165 , tComment :: !(Maybe Text)
166 -- ^ Free-form comments of the author.
167
168 , tCreatedBy :: !(Maybe Text)
169 -- ^ Name and version of the program used to create the .torrent.
170
171 , tCreationDate :: !(Maybe POSIXTime)
172 -- ^ Creation time of the torrent, in standard UNIX epoch.
173
174 , tEncoding :: !(Maybe Text)
175 -- ^ String encoding format used to generate the pieces part of
176 -- the info dictionary in the .torrent metafile.
177
178 , tInfoDict :: !InfoDict
179 -- ^ Info about each content file.
180
181 , tPublisher :: !(Maybe URI)
182 -- ^ Containing the RSA public key of the publisher of the
183 -- torrent. Private counterpart of this key that has the
184 -- authority to allow new peers onto the swarm.
185
186 , tPublisherURL :: !(Maybe URI)
187 , tSignature :: !(Maybe ByteString)
188 -- ^ The RSA signature of the info dictionary (specifically, the
189 -- encrypted SHA-1 hash of the info dictionary).
190 } deriving (Show, Eq, Typeable)
191
192makeLensesFor
193 [ ("tAnnounce" , "announce" )
194 , ("tAnnounceList", "announceList")
195 , ("tComment" , "comment" )
196 , ("tCreatedBy" , "createdBy" )
197 , ("tCreationDate", "creationDate")
198 , ("tEncoding" , "encoding" )
199 , ("tInfoDict" , "infoDict" )
200 , ("tPublisher" , "publisher" )
201 , ("tPublisherURL", "publisherURL")
202 , ("tSignature" , "signature" )
203 ]
204 ''Torrent
205
206instance NFData Torrent where
207 rnf Torrent {..} = rnf tInfoDict
208
209-- TODO move to bencoding
210instance BEncode URI where
211 toBEncode uri = toBEncode (BC.pack (uriToString id uri ""))
212 {-# INLINE toBEncode #-}
213
214 fromBEncode (BString s) | Just url <- parseURI (BC.unpack s) = return url
215 fromBEncode b = decodingError $ "url <" ++ show b ++ ">"
216 {-# INLINE fromBEncode #-}
217
218-- TODO move to bencoding
219instance BEncode POSIXTime where
220 toBEncode pt = toBEncode (floor pt :: Integer)
221 fromBEncode (BInteger i) = return $ fromIntegral i
222 fromBEncode _ = decodingError $ "POSIXTime"
223
224instance BEncode Torrent where
225 toBEncode Torrent {..} = toDict $
226 "announce" .=! tAnnounce
227 .: "announce-list" .=? tAnnounceList
228 .: "comment" .=? tComment
229 .: "created by" .=? tCreatedBy
230 .: "creation date" .=? tCreationDate
231 .: "encoding" .=? tEncoding
232 .: "info" .=! tInfoDict
233 .: "publisher" .=? tPublisher
234 .: "publisher-url" .=? tPublisherURL
235 .: "signature" .=? tSignature
236 .: endDict
237
238 fromBEncode = fromDict $ do
239 Torrent <$>! "announce"
240 <*>? "announce-list"
241 <*>? "comment"
242 <*>? "created by"
243 <*>? "creation date"
244 <*>? "encoding"
245 <*>! "info"
246 <*>? "publisher"
247 <*>? "publisher-url"
248 <*>? "signature"
249
250-- | A simple torrent contains only required fields.
251nullTorrent :: URI -> InfoDict -> Torrent
252nullTorrent ann info = Torrent
253 ann Nothing Nothing Nothing Nothing Nothing
254 info Nothing Nothing Nothing
255
256-- | Extension usually used for torrent metafiles.
257torrentExt :: String
258torrentExt = "torrent"
259
260-- | Test if this path has proper extension.
261isTorrentPath :: FilePath -> Bool
262isTorrentPath filepath = takeExtension filepath == extSeparator : torrentExt
263
264-- | Read and decode a .torrent file.
265fromFile :: FilePath -> IO Torrent
266fromFile filepath = do
267 contents <- BS.readFile filepath
268 case decode contents of
269 Right !t -> return t
270 Left msg -> throwIO $ userError $ msg ++ " while reading torrent file"
271
272toFile :: FilePath -> Torrent -> IO ()
273toFile filepath = BL.writeFile filepath . encode
diff --git a/src/Data/Torrent/InfoHash.hs b/src/Data/Torrent/InfoHash.hs
new file mode 100644
index 00000000..71ea0260
--- /dev/null
+++ b/src/Data/Torrent/InfoHash.hs
@@ -0,0 +1,115 @@
1{-# LANGUAGE FlexibleInstances #-}
2{-# LANGUAGE GeneralizedNewtypeDeriving #-}
3module Data.Torrent.InfoHash
4 ( -- * Info hash
5 InfoHash(..)
6 , addHashToURI
7 , ppInfoHash
8
9
10 , Data.Torrent.InfoHash.hash
11 , Data.Torrent.InfoHash.hashlazy
12 ) where
13
14import Control.Applicative
15import Control.Monad
16import qualified Crypto.Hash.SHA1 as C
17import Data.Aeson
18import Data.BEncode
19import Data.ByteString as BS
20import Data.ByteString.Char8 as BC
21import Data.ByteString.Lazy as BL
22import Data.ByteString.Base16 as Base16
23import qualified Data.ByteString.Lazy.Builder as B
24import qualified Data.ByteString.Lazy.Builder.ASCII as B
25import Data.Char
26import Data.List as L
27import Data.Hashable as Hashable
28import Data.URLEncoded as URL
29import Data.Serialize
30import Data.String
31import Network.URI
32import Numeric
33import Text.ParserCombinators.ReadP as P
34import Text.PrettyPrint
35
36
37-- | Exactly 20 bytes long SHA1 hash of the info part of torrent file.
38newtype InfoHash = InfoHash { getInfoHash :: BS.ByteString }
39 deriving (Eq, Ord, ToJSON, FromJSON)
40
41-- | for hex encoded strings
42instance Show InfoHash where
43 show = render . ppInfoHash
44
45-- | for hex encoded strings
46instance Read InfoHash where
47 readsPrec _ = readP_to_S $ do
48 str <- replicateM 40 (satisfy isHexDigit)
49 return $ InfoHash $ decodeIH str
50 where
51 decodeIH = BS.pack . L.map fromHex . pair
52 fromHex (a, b) = read $ '0' : 'x' : a : b : []
53
54 pair (a : b : xs) = (a, b) : pair xs
55 pair _ = []
56
57-- | for base16 (hex) encoded strings
58instance IsString InfoHash where
59 fromString str
60 | L.length str == 40
61 , (ihStr, inv) <- Base16.decode $ BC.pack str
62 = if BS.length inv == 0 then InfoHash ihStr
63 else error "fromString: invalid infohash string"
64 | otherwise = error "fromString: invalid infohash string length"
65
66instance Hashable InfoHash where
67 hash = Hashable.hash . getInfoHash
68
69instance BEncode InfoHash where
70 toBEncode = toBEncode . getInfoHash
71 fromBEncode be = InfoHash <$> fromBEncode be
72
73instance Serialize InfoHash where
74 put = putByteString . getInfoHash
75 get = InfoHash <$> getBytes 20
76
77instance URLShow InfoHash where
78 urlShow = show
79
80-- | Hash strict bytestring using SHA1 algorithm.
81hash :: BS.ByteString -> InfoHash
82hash = InfoHash . C.hash
83
84-- | Hash lazy bytestring using SHA1 algorithm.
85hashlazy :: BL.ByteString -> InfoHash
86hashlazy = InfoHash . C.hashlazy
87
88-- | Pretty print info hash in hexadecimal format.
89ppInfoHash :: InfoHash -> Doc
90ppInfoHash = text . BC.unpack . ppHex . getInfoHash
91
92ppHex :: BS.ByteString -> BS.ByteString
93ppHex = BL.toStrict . B.toLazyByteString . B.byteStringHexFixed
94
95-- | Add query info hash parameter to uri.
96--
97-- > info_hash=<url_encoded_info_hash>
98--
99addHashToURI :: URI -> InfoHash -> URI
100addHashToURI uri s = uri {
101 uriQuery = uriQuery uri ++ mkPref (uriQuery uri) ++
102 "info_hash=" ++ rfc1738Encode (BC.unpack (getInfoHash s))
103 }
104 where
105 mkPref [] = "?"
106 mkPref ('?' : _) = "&"
107 mkPref _ = error "addHashToURI"
108
109 rfc1738Encode = L.concatMap (\c -> if unreservedS c then [c] else encodeHex c)
110 where
111 unreservedS = (`L.elem` chars)
112 chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_./"
113 encodeHex c = '%' : pHex c
114 pHex c = let p = (showHex . ord $ c) ""
115 in if L.length p == 1 then '0' : p else p
diff --git a/src/Data/Torrent/Layout.hs b/src/Data/Torrent/Layout.hs
new file mode 100644
index 00000000..409426be
--- /dev/null
+++ b/src/Data/Torrent/Layout.hs
@@ -0,0 +1,273 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8{-# LANGUAGE BangPatterns #-}
9{-# LANGUAGE FlexibleInstances #-}
10{-# LANGUAGE StandaloneDeriving #-}
11{-# LANGUAGE GeneralizedNewtypeDeriving #-}
12{-# LANGUAGE DeriveDataTypeable #-}
13{-# LANGUAGE TemplateHaskell #-}
14{-# OPTIONS -fno-warn-orphans #-}
15module Data.Torrent.Layout
16 ( -- * File attribytes
17 FileOffset
18 , FileSize
19
20 -- * Single file info
21 , FileInfo (..)
22 , fileLength
23 , filePath
24 , fileMD5Sum
25
26 -- * File layout
27 , LayoutInfo (..)
28 , singleFile
29 , multiFile
30 , rootDirName
31 , isSingleFile
32 , isMultiFile
33 , fileNumber
34 , contentLength
35 , blockCount
36
37 -- * Flat file layout
38 , Layout
39 , flatLayout
40 , accumOffsets
41 , fileOffset
42
43 -- * Internal
44 , getLayoutInfo
45 , putLayoutInfo
46 ) where
47
48import Control.Applicative
49import Control.DeepSeq
50import Control.Lens
51import Data.Aeson.TH
52import Data.Aeson.Types (FromJSON, ToJSON)
53import Data.BEncode
54import Data.BEncode.Types
55import Data.ByteString as BS
56import Data.ByteString.Char8 as BC
57import Data.Char
58import Data.List as L
59import Data.Typeable
60import System.FilePath
61import System.Posix.Types
62
63
64{-----------------------------------------------------------------------
65-- File attribytes
66-----------------------------------------------------------------------}
67
68type FileSize = FileOffset
69
70deriving instance FromJSON FileOffset
71deriving instance ToJSON FileOffset
72deriving instance BEncode FileOffset
73
74{-----------------------------------------------------------------------
75-- File info both either from info dict or file list
76-----------------------------------------------------------------------}
77
78-- | Contain info about one single file.
79data FileInfo a = FileInfo {
80 fiLength :: {-# UNPACK #-} !FileSize
81 -- ^ Length of the file in bytes.
82
83 -- TODO unpacked MD5 sum
84 , fiMD5Sum :: !(Maybe ByteString)
85 -- ^ 32 character long MD5 sum of the file. Used by third-party
86 -- tools, not by bittorrent protocol itself.
87
88 , fiName :: !a
89 -- ^ One or more string elements that together represent the
90 -- path and filename. Each element in the list corresponds to
91 -- either a directory name or (in the case of the last
92 -- element) the filename. For example, the file:
93 --
94 -- > "dir1/dir2/file.ext"
95 --
96 -- would consist of three string elements:
97 --
98 -- > ["dir1", "dir2", "file.ext"]
99 --
100 } deriving (Show, Read, Eq, Typeable)
101
102$(deriveJSON (L.map toLower . L.dropWhile isLower) ''FileInfo)
103
104makeLensesFor
105 [ ("fiLength", "fileLength")
106 , ("fiMD5Sum", "fileMD5Sum")
107 , ("fiName" , "filePath" )
108 ]
109 ''FileInfo
110
111instance NFData a => NFData (FileInfo a) where
112 rnf FileInfo {..} = rnf fiName
113 {-# INLINE rnf #-}
114
115instance BEncode (FileInfo [ByteString]) where
116 toBEncode FileInfo {..} = toDict $
117 "length" .=! fiLength
118 .: "md5sum" .=? fiMD5Sum
119 .: "path" .=! fiName
120 .: endDict
121 {-# INLINE toBEncode #-}
122
123 fromBEncode = fromDict $ do
124 FileInfo <$>! "length"
125 <*>? "md5sum"
126 <*>! "path"
127 {-# INLINE fromBEncode #-}
128
129type Put a = a -> BDict -> BDict
130
131putFileInfoSingle :: Put (FileInfo ByteString)
132putFileInfoSingle FileInfo {..} cont =
133 "length" .=! fiLength
134 .: "md5sum" .=? fiMD5Sum
135 .: "name" .=! fiName
136 .: cont
137
138getFileInfoSingle :: Get (FileInfo ByteString)
139getFileInfoSingle = do
140 FileInfo <$>! "length"
141 <*>? "md5sum"
142 <*>! "name"
143
144instance BEncode (FileInfo ByteString) where
145 toBEncode = toDict . (`putFileInfoSingle` endDict)
146 {-# INLINE toBEncode #-}
147
148 fromBEncode = fromDict getFileInfoSingle
149 {-# INLINE fromBEncode #-}
150
151{-----------------------------------------------------------------------
152-- Original torrent file layout info
153-----------------------------------------------------------------------}
154
155data LayoutInfo
156 = SingleFile
157 { liFile :: !(FileInfo ByteString)
158 }
159 | MultiFile
160 { -- | List of the all files that torrent contains.
161 liFiles :: ![FileInfo [ByteString]]
162
163 -- | The /suggested/ name of the root directory in which to
164 -- store all the files.
165 , liDirName :: !ByteString
166 } deriving (Show, Read, Eq, Typeable)
167
168$(deriveJSON (L.map toLower . L.dropWhile isLower) ''LayoutInfo)
169
170makeLensesFor
171 [ ("liFile" , "singleFile" )
172 , ("liFiles" , "multiFile" )
173 , ("liDirName", "rootDirName")
174 ]
175 ''LayoutInfo
176
177instance NFData LayoutInfo where
178 rnf SingleFile {..} = ()
179 rnf MultiFile {..} = rnf liFiles
180
181getLayoutInfo :: Get LayoutInfo
182getLayoutInfo = single <|> multi
183 where
184 single = SingleFile <$> getFileInfoSingle
185 multi = MultiFile <$>! "files" <*>! "name"
186
187putLayoutInfo :: Put LayoutInfo
188putLayoutInfo SingleFile {..} = putFileInfoSingle liFile
189putLayoutInfo MultiFile {..} = \ cont ->
190 "files" .=! liFiles
191 .: "name" .=! liDirName
192 .: cont
193
194instance BEncode LayoutInfo where
195 toBEncode = toDict . (`putLayoutInfo` endDict)
196 fromBEncode = fromDict getLayoutInfo
197
198-- | Test if this is single file torrent.
199isSingleFile :: LayoutInfo -> Bool
200isSingleFile SingleFile {} = True
201isSingleFile _ = False
202{-# INLINE isSingleFile #-}
203
204-- | Test if this is multifile torrent.
205isMultiFile :: LayoutInfo -> Bool
206isMultiFile MultiFile {} = True
207isMultiFile _ = False
208{-# INLINE isMultiFile #-}
209
210-- | Find sum of sizes of the all torrent files.
211contentLength :: LayoutInfo -> FileSize
212contentLength SingleFile { liFile = FileInfo {..} } = fiLength
213contentLength MultiFile { liFiles = tfs } = sum (L.map fiLength tfs)
214
215-- | Get count of all files in torrent.
216fileNumber :: LayoutInfo -> Int
217fileNumber SingleFile {..} = 1
218fileNumber MultiFile {..} = L.length liFiles
219
220-- | Find number of blocks of the specified size. If torrent size is
221-- not a multiple of block size then the count is rounded up.
222blockCount :: Int -- ^ Block size.
223 -> LayoutInfo -- ^ Torrent content info.
224 -> Int -- ^ Number of blocks.
225blockCount blkSize ci = contentLength ci `sizeInBase` blkSize
226
227{-----------------------------------------------------------------------
228-- Flat layout
229-----------------------------------------------------------------------}
230
231-- | File layout specifies the order and the size of each file in the
232-- storage. Note that order of files is highly important since we
233-- coalesce all the files in the given order to get the linear block
234-- address space.
235--
236type Layout a = [(FilePath, a)]
237
238-- | Extract files layout from torrent info with the given root path.
239flatLayout
240 :: FilePath -- ^ Root path for the all torrent files.
241 -> LayoutInfo -- ^ Torrent content information.
242 -> Layout FileSize -- ^ The all file paths prefixed with the given root.
243flatLayout prefixPath SingleFile { liFile = FileInfo {..} }
244 = [(prefixPath </> BC.unpack fiName, fiLength)]
245flatLayout prefixPath MultiFile {..} = L.map mkPath liFiles
246 where -- TODO use utf8 encoding in name
247 mkPath FileInfo {..} = (path, fiLength)
248 where
249 path = prefixPath </> BC.unpack liDirName
250 </> joinPath (L.map BC.unpack fiName)
251
252accumOffsets :: Layout FileSize -> Layout FileOffset
253accumOffsets = go 0
254 where
255 go !_ [] = []
256 go !offset ((n, s) : xs) = (n, offset) : go (offset + s) xs
257
258-- | Gives global offset of a content file for a given full path.
259fileOffset :: FilePath -> Layout FileOffset -> Maybe FileOffset
260fileOffset = lookup
261{-# INLINE fileOffset #-}
262
263{-----------------------------------------------------------------------
264-- Internal utilities
265-----------------------------------------------------------------------}
266
267-- | Divide and round up.
268sizeInBase :: Integral a => a -> Int -> Int
269sizeInBase n b = fromIntegral (n `div` fromIntegral b) + align
270 where
271 align = if n `mod` fromIntegral b == 0 then 0 else 1
272{-# SPECIALIZE sizeInBase :: Int -> Int -> Int #-}
273{-# SPECIALIZE sizeInBase :: Integer -> Int -> Int #-}
diff --git a/src/Data/Torrent/Magnet.hs b/src/Data/Torrent/Magnet.hs
new file mode 100644
index 00000000..df928b66
--- /dev/null
+++ b/src/Data/Torrent/Magnet.hs
@@ -0,0 +1,236 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- Parsing and rendering of magnet URIs.
9--
10-- For more info see:
11-- <http://magnet-uri.sourceforge.net/magnet-draft-overview.txt>
12--
13-- Bittorrent specific info:
14-- <http://www.bittorrent.org/beps/bep_0009.html>
15--
16module Data.Torrent.Magnet
17 ( -- * Magnet
18 Magnet(..)
19 , nullMagnet
20 , parseMagnet
21 , renderMagnet
22
23 -- ** Extra
24 , fromURI
25 , toURI
26 ) where
27
28import Control.Applicative
29import Control.Monad
30import Data.ByteString as BS
31import Data.ByteString.Base16 as Base16
32import Data.ByteString.Base32 as Base32
33import Data.Map as M
34import Data.Maybe
35import Data.List as L
36import Data.URLEncoded as URL
37import Data.String
38import Data.Text as T
39import Data.Text.Encoding as T
40import Network.URI
41import Text.Read
42
43import Data.Torrent.InfoHash
44
45
46{-----------------------------------------------------------------------
47-- URN
48-----------------------------------------------------------------------}
49
50type NamespaceId = [Text]
51
52btih :: NamespaceId
53btih = ["btih"]
54
55-- | Uniform Resource Name - location-independent, resource
56-- identifier.
57data URN = URN
58 { urnNamespace :: NamespaceId
59 , urnString :: Text
60 } deriving (Eq, Ord)
61
62instance Show URN where
63 showsPrec n = showsPrec n . T.unpack . renderURN
64
65instance IsString URN where
66 fromString = fromMaybe def . parseURN . T.pack
67 where
68 def = error "unable to parse URN"
69
70instance URLShow URN where
71 urlShow = T.unpack . renderURN
72
73parseURN :: Text -> Maybe URN
74parseURN str = case T.split (== ':') str of
75 uriScheme : body
76 | T.toLower uriScheme == "urn" -> mkURN body
77 | otherwise -> Nothing
78 [] -> Nothing
79 where
80 mkURN [] = Nothing
81 mkURN xs = Just $ URN
82 { urnNamespace = L.init xs
83 , urnString = L.last xs
84 }
85
86renderURN :: URN -> Text
87renderURN URN {..}
88 = T.intercalate ":" $ "urn" : urnNamespace ++ [urnString]
89
90urnToInfoHash :: URN -> Maybe InfoHash
91urnToInfoHash (URN {..})
92 | urnNamespace /= btih = Nothing
93 | hashLen == 20 = Just $ InfoHash hashStr
94 | hashLen == 32 = Just $ InfoHash $ Base32.decode hashStr
95 | hashLen == 40 = let (ihStr, inv) = Base16.decode hashStr
96 in if BS.length inv == 0
97 then Just $ InfoHash ihStr
98 else Nothing
99 | otherwise = Nothing
100 where
101 hashLen = BS.length hashStr
102 hashStr = T.encodeUtf8 urnString
103
104infoHashToURN :: InfoHash -> URN
105infoHashToURN = URN btih . T.pack . show
106
107{-----------------------------------------------------------------------
108-- Magnet
109-----------------------------------------------------------------------}
110
111-- TODO multiple exact topics
112-- TODO supplement
113
114-- | An URI used to identify torrent.
115data Magnet = Magnet
116 { -- | Resource hash.
117 exactTopic :: !InfoHash
118 -- | Might be used to display name while waiting for metadata.
119 , displayName :: Maybe Text
120 -- | Size of the resource in bytes.
121 , exactLength :: Maybe Integer
122
123 , manifest :: Maybe String
124 -- | Search string.
125 , keywordTopic :: Maybe String
126
127 , acceptableSource :: Maybe URI
128 , exactSource :: Maybe URI
129
130 , tracker :: Maybe URI
131
132 , supplement :: Map Text Text
133 } deriving (Eq, Ord)
134
135instance Show Magnet where
136 show = renderMagnet
137 {-# INLINE show #-}
138
139instance Read Magnet where
140 readsPrec _ xs
141 | Just m <- parseMagnet mstr = [(m, rest)]
142 | otherwise = []
143 where
144 (mstr, rest) = L.break (== ' ') xs
145
146instance IsString Magnet where
147 fromString = fromMaybe def . parseMagnet
148 where
149 def = error "unable to parse magnet"
150
151instance URLEncode Magnet where
152 urlEncode = toQuery
153 {-# INLINE urlEncode #-}
154
155-- | Set exact topic only, other params are empty.
156nullMagnet :: InfoHash -> Magnet
157nullMagnet u = Magnet
158 { exactTopic = u
159 , displayName = Nothing
160 , exactLength = Nothing
161 , manifest = Nothing
162 , keywordTopic = Nothing
163 , acceptableSource = Nothing
164 , exactSource = Nothing
165 , tracker = Nothing
166 , supplement = M.empty
167 }
168
169fromQuery :: URLEncoded -> Either String Magnet
170fromQuery q
171 | Just urnStr <- URL.lookup ("xt" :: String) q
172 , Just urn <- parseURN $ T.pack urnStr
173 , Just infoHash <- urnToInfoHash urn
174 = return $ Magnet
175 { exactTopic = infoHash
176 , displayName = T.pack <$> URL.lookup ("dn" :: String) q
177 , exactLength = readMaybe =<< URL.lookup ("xl" :: String) q
178
179 , manifest = URL.lookup ("mt" :: String) q
180 , keywordTopic = URL.lookup ("kt" :: String) q
181
182 , acceptableSource = parseURI =<< URL.lookup ("as" :: String) q
183 , exactSource = parseURI =<< URL.lookup ("xs" :: String) q
184
185 , tracker = parseURI =<< URL.lookup ("tr" :: String) q
186 , supplement = M.empty
187 }
188
189 | otherwise = Left "exact topic not defined"
190
191toQuery :: Magnet -> URLEncoded
192toQuery Magnet {..}
193 = s "xt" %= infoHashToURN exactTopic
194 %& s "dn" %=? (T.unpack <$> displayName)
195 %& s "xl" %=? exactLength
196 %& s "mt" %=? manifest
197 %& s "kt" %=? keywordTopic
198 %& s "as" %=? acceptableSource
199 %& s "xs" %=? exactSource
200 %& s "tr" %=? tracker
201 where
202 s :: String -> String; s = id
203
204magnetScheme :: URI
205magnetScheme = URI
206 { uriScheme = "magnet:"
207 , uriAuthority = Nothing
208 , uriPath = ""
209 , uriQuery = ""
210 , uriFragment = ""
211 }
212
213isMagnetURI :: URI -> Bool
214isMagnetURI u = u { uriQuery = "" } == magnetScheme
215
216-- | The same as 'parseMagnet' but useful if you alread have a parsed
217-- uri.
218fromURI :: URI -> Either String Magnet
219fromURI u @ URI {..}
220 | not (isMagnetURI u) = Left "this is not a magnet link"
221 | otherwise = importURI u >>= fromQuery
222
223-- | The same as 'renderMagnet' but useful if you need an uri.
224toURI :: Magnet -> URI
225toURI m = magnetScheme %? urlEncode m
226
227etom :: Either a b -> Maybe b
228etom = either (const Nothing) Just
229
230-- | Try to parse magnet link from urlencoded string.
231parseMagnet :: String -> Maybe Magnet
232parseMagnet = parseURI >=> etom . fromURI
233
234-- | Render magnet link to urlencoded string
235renderMagnet :: Magnet -> String
236renderMagnet = show . toURI
diff --git a/src/Data/Torrent/Piece.hs b/src/Data/Torrent/Piece.hs
new file mode 100644
index 00000000..ea4e6253
--- /dev/null
+++ b/src/Data/Torrent/Piece.hs
@@ -0,0 +1,203 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8{-# LANGUAGE TemplateHaskell #-}
9{-# LANGUAGE DeriveDataTypeable #-}
10{-# LANGUAGE GeneralizedNewtypeDeriving #-}
11module Data.Torrent.Piece
12 ( -- * Piece attributes
13 -- ** Piece size
14 PieceSize (..)
15 , defaultBlockSize -- TODO use data-default
16 , optimalPieceCount
17 , defaultPieceSize -- TODO use data-default
18
19 -- ** Piece index
20 , PieceIx
21
22 -- * Piece data
23 , Piece (..)
24 , ppPiece
25
26 -- * Piece control
27 , PieceInfo (..)
28 , ppPieceInfo
29 , pieceLength
30 , pieceHashes
31 , pieceHash
32 , pieceCount
33 , checkPieceLazy
34
35
36 -- * Internal
37 , getPieceInfo
38 , putPieceInfo
39 ) where
40
41import Control.DeepSeq
42import Control.Lens
43import qualified Crypto.Hash.SHA1 as SHA1
44import Data.Aeson (ToJSON, FromJSON)
45import Data.Aeson.TH
46import Data.BEncode
47import Data.BEncode.Types
48import Data.Bits
49import Data.Bits.Extras
50import Data.ByteString as BS
51import qualified Data.ByteString.Lazy as BL
52import Data.Char
53import Data.Int
54import Data.List as L
55import Data.Typeable
56import Text.PrettyPrint
57
58
59class Lint a where
60 lint :: a -> Either String a
61
62type PieceCount = Int -- TODO newtype
63type PieceIx = Int -- TODO remove
64
65newtype PieceIndex = PieceIndex Int
66
67-- | An int used to denote piece size.
68newtype PieceSize = PieceSize Int
69 deriving (Show, Read, Typeable
70 , Eq, Ord, Enum
71 , Num, Real, Integral
72 , BEncode, ToJSON, FromJSON
73 )
74
75-- | Widely used semi-official block size.
76defaultBlockSize :: Int
77defaultBlockSize = 16 * 1024
78
79maxPieceSize :: Int
80maxPieceSize = 4 * 1024 * 1024
81{-# INLINE maxPieceSize #-}
82
83minPieceSize :: Int
84minPieceSize = defaultBlockSize * 4
85{-# INLINE minPieceSize #-}
86
87-- | NOTE: Have max and min size constrained to wide used
88-- semi-standard values. This bounds should be used to make decision
89-- about piece size for new torrents.
90--
91instance Bounded PieceSize where
92 maxBound = PieceSize maxPieceSize
93 {-# INLINE maxBound #-}
94
95 minBound = PieceSize minPieceSize
96 {-# INLINE minBound #-}
97
98-- | TODO
99optimalPieceCount :: Int
100optimalPieceCount = 1000
101{-# INLINE optimalPieceCount #-}
102
103toPow2 :: Int -> Int
104toPow2 x = bit $ fromIntegral (leadingZeros (0 :: Int) - leadingZeros x)
105
106-- | Find the optimal piece size for a given torrent size.
107defaultPieceSize :: Int64 -> Int
108defaultPieceSize x = max minPieceSize $ min maxPieceSize $ toPow2 pc
109 where
110 pc = fromIntegral (x `div` fromIntegral optimalPieceCount)
111
112-- TODO check if pieceLength is power of 2
113-- | Piece payload should be strict or lazy bytestring.
114data Piece a = Piece
115 { -- | Zero-based piece index in torrent. TODO how pieces are indexed?
116 pieceIndex :: {-# UNPACK #-} !PieceIx
117 -- | Payload.
118 , pieceData :: !a
119 } deriving (Show, Read, Eq, Typeable)
120
121$(deriveJSON (L.map toLower . L.dropWhile isLower) ''Piece)
122
123instance NFData (Piece a)
124
125-- | Format piece in human readable form. Payload bytes are omitted.
126ppPiece :: Piece a -> Doc
127ppPiece Piece {..}
128 = "Piece" <+> braces ("index" <+> "=" <+> int pieceIndex)
129
130data PieceInfo = PieceInfo
131 { piPieceLength :: {-# UNPACK #-} !PieceSize
132 -- ^ Number of bytes in each piece.
133
134 , piPieceHashes :: !ByteString
135 -- ^ Concatenation of all 20-byte SHA1 hash values.
136 } deriving (Show, Read, Eq, Typeable)
137
138$(deriveJSON (L.map toLower . L.dropWhile isLower) ''PieceInfo)
139
140-- | Number of bytes in each piece.
141makeLensesFor [("piPieceLength", "pieceLength")] ''PieceInfo
142
143-- | Concatenation of all 20-byte SHA1 hash values.
144makeLensesFor [("piPieceHashes", "pieceHashes")] ''PieceInfo
145
146instance NFData PieceInfo
147
148instance Lint PieceInfo where
149 lint pinfo @ PieceInfo {..}
150 | BS.length piPieceHashes `rem` hashsize == 0
151 , piPieceLength >= 0 = return pinfo
152 | otherwise = Left undefined
153
154
155putPieceInfo :: PieceInfo -> BDict -> BDict
156putPieceInfo PieceInfo {..} cont =
157 "piece length" .=! piPieceLength
158 .: "pieces" .=! piPieceHashes
159 .: cont
160
161getPieceInfo :: Get PieceInfo
162getPieceInfo = do
163 PieceInfo <$>! "piece length"
164 <*>! "pieces"
165
166instance BEncode PieceInfo where
167 toBEncode = toDict . (`putPieceInfo` endDict)
168 fromBEncode = fromDict getPieceInfo
169
170-- | Format piece info in human readable form. Hashes are omitted.
171ppPieceInfo :: PieceInfo -> Doc
172ppPieceInfo PieceInfo { piPieceLength = PieceSize len } =
173 "PieceInfo" <+> braces ("length" <+> "=" <+> int len)
174
175hashsize :: Int
176hashsize = 20
177{-# INLINE hashsize #-}
178
179slice :: Int -> Int -> ByteString -> ByteString
180slice start len = BS.take len . BS.drop start
181{-# INLINE slice #-}
182
183-- | Extract validation hash by specified piece index.
184pieceHash :: PieceInfo -> PieceIx -> ByteString
185pieceHash PieceInfo {..} i = slice (hashsize * i) hashsize piPieceHashes
186
187-- | Find count of pieces in the torrent. If torrent size is not a
188-- multiple of piece size then the count is rounded up.
189pieceCount :: PieceInfo -> PieceCount
190pieceCount PieceInfo {..} = BS.length piPieceHashes `quot` hashsize
191
192isLastPiece :: PieceInfo -> PieceIx -> Bool
193isLastPiece ci i = pieceCount ci == succ i
194
195class Validation a where
196 validate :: PieceInfo -> Piece a -> Bool
197
198-- | Validate piece with metainfo hash.
199checkPieceLazy :: PieceInfo -> Piece BL.ByteString -> Bool
200checkPieceLazy pinfo @ PieceInfo {..} Piece {..}
201 = (fromIntegral (BL.length pieceData) == piPieceLength
202 || isLastPiece pinfo pieceIndex)
203 && SHA1.hashlazy pieceData == pieceHash pinfo pieceIndex
diff --git a/src/Data/Torrent/Tree.hs b/src/Data/Torrent/Tree.hs
new file mode 100644
index 00000000..e9a337a1
--- /dev/null
+++ b/src/Data/Torrent/Tree.hs
@@ -0,0 +1,71 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8{-# LANGUAGE FlexibleInstances #-}
9{-# LANGUAGE TemplateHaskell #-}
10{-# LANGUAGE DeriveDataTypeable #-}
11module Data.Torrent.Tree
12 ( DirTree (..)
13 , build
14
15 , Data.Torrent.Tree.lookup
16 , lookupDir
17
18 , fileCount
19 , dirCount
20 ) where
21
22import Control.Arrow
23import Data.ByteString as BS
24import Data.ByteString.Char8 as BC
25import Data.Foldable
26import Data.List as L
27import Data.Map as M
28import Data.Monoid
29
30import Data.Torrent.Layout
31
32
33data DirTree a = Dir { children :: Map ByteString (DirTree a) }
34 | File { node :: FileInfo a }
35 deriving Show
36
37build :: LayoutInfo -> DirTree ()
38build SingleFile {liFile = FileInfo {..}} = Dir
39 { children = M.singleton fiName (File fi) }
40 where
41 fi = FileInfo fiLength fiMD5Sum ()
42build MultiFile {..} = Dir $ M.singleton liDirName files
43 where
44 files = Dir $ M.fromList $ L.map mkFileEntry liFiles
45 mkFileEntry FileInfo {..} = (L.head fiName, ent) -- TODO FIXME
46 where
47 ent = File $ FileInfo fiLength fiMD5Sum ()
48
49decompress :: DirTree () -> [FileInfo ()]
50decompress = undefined
51
52lookup :: [FilePath] -> DirTree a -> Maybe (DirTree a)
53lookup [] t = Just t
54lookup (p : ps) (Dir m) | Just subTree <- M.lookup (BC.pack p) m
55 = Data.Torrent.Tree.lookup ps subTree
56lookup _ _ = Nothing
57
58lookupDir :: [FilePath] -> DirTree a -> Maybe [(ByteString, DirTree a)]
59lookupDir ps d
60 | Just subTree <- Data.Torrent.Tree.lookup ps d =
61 case subTree of
62 File _ -> Nothing
63 Dir es -> Just $ M.toList es
64
65fileCount :: DirTree a -> Sum Int
66fileCount File {..} = Sum 1
67fileCount Dir {..} = foldMap fileCount children
68
69dirCount :: DirTree a -> Sum Int
70dirCount File {..} = Sum 0
71dirCount Dir {..} = Sum 1 <> foldMap dirCount children