diff options
author | Sam Truzjan <pxqr.sta@gmail.com> | 2013-10-31 11:25:59 +0400 |
---|---|---|
committer | Sam Truzjan <pxqr.sta@gmail.com> | 2013-10-31 11:25:59 +0400 |
commit | 01cef3fafc27d39d88c94cacdcd8e204c5f66b86 (patch) | |
tree | 01040aca19e49f4e7937383fef53b8c82bcec12b /src/Data/Torrent.hs | |
parent | c1fec260f47084300ac30de2e43d52966316a2c7 (diff) |
Merge bittorrent package with torrent-content
Diffstat (limited to 'src/Data/Torrent.hs')
-rw-r--r-- | src/Data/Torrent.hs | 273 |
1 files changed, 273 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 | ||
27 | module 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 | |||
64 | import Prelude hiding (sum) | ||
65 | |||
66 | import Control.Applicative | ||
67 | import Control.DeepSeq | ||
68 | import Control.Exception | ||
69 | import Control.Lens | ||
70 | |||
71 | import Data.Aeson.TH | ||
72 | import Data.BEncode as BE | ||
73 | import Data.BEncode.Types as BE | ||
74 | import Data.ByteString as BS | ||
75 | import qualified Data.ByteString.Char8 as BC (pack, unpack) | ||
76 | import qualified Data.ByteString.Lazy as BL | ||
77 | import Data.Char | ||
78 | import Data.Hashable as Hashable | ||
79 | import qualified Data.List as L | ||
80 | import Data.Text (Text) | ||
81 | import Data.Time.Clock.POSIX | ||
82 | import Data.Typeable | ||
83 | import Network.URI | ||
84 | import System.FilePath | ||
85 | |||
86 | import Data.Torrent.InfoHash as IH | ||
87 | import Data.Torrent.Layout | ||
88 | import 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. | ||
100 | data 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 | |||
115 | makeLensesFor | ||
116 | [ ("idInfoHash" , "infohash" ) | ||
117 | , ("idLayoutInfo", "layoutInfo") | ||
118 | , ("idPieceInfo" , "pieceInfo" ) | ||
119 | , ("idPrivate" , "isPrivate" ) | ||
120 | ] | ||
121 | ''InfoDict | ||
122 | |||
123 | instance NFData InfoDict where | ||
124 | rnf InfoDict {..} = rnf idLayoutInfo | ||
125 | |||
126 | instance Hashable InfoDict where | ||
127 | hash = Hashable.hash . idInfoHash | ||
128 | {-# INLINE hash #-} | ||
129 | |||
130 | getPrivate :: Get Bool | ||
131 | getPrivate = (Just True ==) <$>? "private" | ||
132 | |||
133 | putPrivate :: Bool -> BDict -> BDict | ||
134 | putPrivate False = id | ||
135 | putPrivate True = \ cont -> "private" .=! True .: cont | ||
136 | |||
137 | instance 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. | ||
156 | data 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 | |||
192 | makeLensesFor | ||
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 | |||
206 | instance NFData Torrent where | ||
207 | rnf Torrent {..} = rnf tInfoDict | ||
208 | |||
209 | -- TODO move to bencoding | ||
210 | instance 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 | ||
219 | instance BEncode POSIXTime where | ||
220 | toBEncode pt = toBEncode (floor pt :: Integer) | ||
221 | fromBEncode (BInteger i) = return $ fromIntegral i | ||
222 | fromBEncode _ = decodingError $ "POSIXTime" | ||
223 | |||
224 | instance 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. | ||
251 | nullTorrent :: URI -> InfoDict -> Torrent | ||
252 | nullTorrent ann info = Torrent | ||
253 | ann Nothing Nothing Nothing Nothing Nothing | ||
254 | info Nothing Nothing Nothing | ||
255 | |||
256 | -- | Extension usually used for torrent metafiles. | ||
257 | torrentExt :: String | ||
258 | torrentExt = "torrent" | ||
259 | |||
260 | -- | Test if this path has proper extension. | ||
261 | isTorrentPath :: FilePath -> Bool | ||
262 | isTorrentPath filepath = takeExtension filepath == extSeparator : torrentExt | ||
263 | |||
264 | -- | Read and decode a .torrent file. | ||
265 | fromFile :: FilePath -> IO Torrent | ||
266 | fromFile 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 | |||
272 | toFile :: FilePath -> Torrent -> IO () | ||
273 | toFile filepath = BL.writeFile filepath . encode | ||