diff options
Diffstat (limited to 'src/Data')
-rw-r--r-- | src/Data/Torrent.hs | 989 | ||||
-rw-r--r-- | src/Data/Torrent/Bitfield.hs | 324 | ||||
-rw-r--r-- | src/Data/Torrent/InfoHash.hs | 164 | ||||
-rw-r--r-- | src/Data/Torrent/Layout.hs | 321 | ||||
-rw-r--r-- | src/Data/Torrent/Magnet.hs | 372 | ||||
-rw-r--r-- | src/Data/Torrent/Piece.hs | 232 | ||||
-rw-r--r-- | src/Data/Torrent/Progress.hs | 155 | ||||
-rw-r--r-- | src/Data/Torrent/Tree.hs | 83 |
8 files changed, 955 insertions, 1685 deletions
diff --git a/src/Data/Torrent.hs b/src/Data/Torrent.hs index b233937b..cfc26453 100644 --- a/src/Data/Torrent.hs +++ b/src/Data/Torrent.hs | |||
@@ -17,23 +17,91 @@ | |||
17 | -- <https://wiki.theory.org/BitTorrentSpecification#Metainfo_File_Structure> | 17 | -- <https://wiki.theory.org/BitTorrentSpecification#Metainfo_File_Structure> |
18 | -- | 18 | -- |
19 | {-# LANGUAGE CPP #-} | 19 | {-# LANGUAGE CPP #-} |
20 | {-# LANGUAGE NamedFieldPuns #-} | ||
20 | {-# LANGUAGE FlexibleInstances #-} | 21 | {-# LANGUAGE FlexibleInstances #-} |
21 | {-# LANGUAGE OverlappingInstances #-} | 22 | {-# LANGUAGE OverlappingInstances #-} |
23 | {-# LANGUAGE MultiParamTypeClasses #-} | ||
22 | {-# LANGUAGE BangPatterns #-} | 24 | {-# LANGUAGE BangPatterns #-} |
23 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} | 25 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} |
26 | {-# LANGUAGE StandaloneDeriving #-} | ||
24 | {-# LANGUAGE DeriveDataTypeable #-} | 27 | {-# LANGUAGE DeriveDataTypeable #-} |
28 | {-# LANGUAGE DeriveFunctor #-} | ||
29 | {-# LANGUAGE DeriveFoldable #-} | ||
30 | {-# LANGUAGE DeriveTraversable #-} | ||
25 | {-# LANGUAGE TemplateHaskell #-} | 31 | {-# LANGUAGE TemplateHaskell #-} |
26 | {-# OPTIONS -fno-warn-orphans #-} | 32 | {-# OPTIONS -fno-warn-orphans #-} |
27 | module Data.Torrent | 33 | module Data.Torrent |
28 | ( -- * Info dictionary | 34 | ( -- * InfoHash |
29 | InfoDict (..) | 35 | -- $infohash |
30 | , infoDictionary | 36 | InfoHash |
37 | , textToInfoHash | ||
38 | , longHex | ||
39 | , shortHex | ||
31 | 40 | ||
32 | -- ** Lenses | 41 | -- * File layout |
42 | -- ** FileInfo | ||
43 | , FileOffset | ||
44 | , FileSize | ||
45 | , FileInfo (..) | ||
46 | , fileLength | ||
47 | , filePath | ||
48 | , fileMD5Sum | ||
49 | |||
50 | -- ** Layout info | ||
51 | , LayoutInfo (..) | ||
52 | , singleFile | ||
53 | , multiFile | ||
54 | , rootDirName | ||
55 | , joinFilePath | ||
56 | , isSingleFile | ||
57 | , isMultiFile | ||
58 | , suggestedName | ||
59 | , contentLength | ||
60 | , fileCount | ||
61 | , blockCount | ||
62 | |||
63 | -- ** Flat layout info | ||
64 | , FileLayout | ||
65 | , flatLayout | ||
66 | , accumPositions | ||
67 | , fileOffset | ||
68 | |||
69 | -- ** Internal | ||
70 | , sizeInBase | ||
71 | |||
72 | -- * Pieces | ||
73 | -- ** Attributes | ||
74 | , PieceIx | ||
75 | , PieceCount | ||
76 | , PieceSize | ||
77 | , minPieceSize | ||
78 | , maxPieceSize | ||
79 | , defaultPieceSize | ||
80 | , PieceHash | ||
81 | |||
82 | -- ** Piece data | ||
83 | , Piece (..) | ||
84 | , pieceSize | ||
85 | , hashPiece | ||
86 | |||
87 | -- ** Piece control | ||
88 | , HashList (..) | ||
89 | , PieceInfo (..) | ||
90 | , pieceLength | ||
91 | , pieceHashes | ||
92 | , pieceCount | ||
93 | |||
94 | -- ** Validation | ||
95 | , pieceHash | ||
96 | , checkPieceLazy | ||
97 | |||
98 | -- * Info dictionary | ||
99 | , InfoDict (..) | ||
33 | , infohash | 100 | , infohash |
34 | , layoutInfo | 101 | , layoutInfo |
35 | , pieceInfo | 102 | , pieceInfo |
36 | , isPrivate | 103 | , isPrivate |
104 | , infoDictionary | ||
37 | 105 | ||
38 | -- * Torrent file | 106 | -- * Torrent file |
39 | , Torrent(..) | 107 | , Torrent(..) |
@@ -50,50 +118,598 @@ module Data.Torrent | |||
50 | , publisherURL | 118 | , publisherURL |
51 | , signature | 119 | , signature |
52 | 120 | ||
53 | -- * Construction | 121 | -- ** Utils |
54 | , nullTorrent | 122 | , nullTorrent |
55 | |||
56 | -- * Mime types | ||
57 | , typeTorrent | 123 | , typeTorrent |
58 | |||
59 | -- * File paths | ||
60 | , torrentExt | 124 | , torrentExt |
61 | , isTorrentPath | 125 | , isTorrentPath |
62 | |||
63 | -- * IO | ||
64 | , fromFile | 126 | , fromFile |
65 | , toFile | 127 | , toFile |
128 | |||
129 | -- * Magnet | ||
130 | -- $magnet-link | ||
131 | , Magnet(..) | ||
132 | , nullMagnet | ||
133 | , simpleMagnet | ||
134 | , detailedMagnet | ||
135 | , parseMagnet | ||
136 | , renderMagnet | ||
137 | |||
138 | -- ** URN | ||
139 | , URN (..) | ||
140 | , NamespaceId | ||
141 | , btih | ||
142 | , infohashURN | ||
143 | , parseURN | ||
144 | , renderURN | ||
66 | ) where | 145 | ) where |
67 | 146 | ||
68 | import Prelude hiding (sum) | 147 | import Prelude |
69 | import Control.Applicative | 148 | import Control.Applicative |
70 | import qualified Crypto.Hash.SHA1 as C | ||
71 | import Control.DeepSeq | 149 | import Control.DeepSeq |
72 | import Control.Exception | 150 | import Control.Exception |
73 | import Control.Lens | 151 | import Control.Lens |
152 | import Control.Monad | ||
153 | import Crypto.Hash.SHA1 as SHA1 | ||
74 | import Data.BEncode as BE | 154 | import Data.BEncode as BE |
75 | import Data.BEncode.Types as BE | 155 | import Data.BEncode.Types as BE |
76 | import Data.ByteString as BS | 156 | import Data.Bits |
77 | import qualified Data.ByteString.Char8 as BC (pack, unpack) | 157 | import Data.Bits.Extras |
78 | import qualified Data.ByteString.Lazy as BL | 158 | import Data.ByteString as BS |
79 | import Data.Convertible | 159 | import Data.ByteString.Base16 as Base16 |
80 | import Data.Default | 160 | import Data.ByteString.Base32 as Base32 |
81 | import Data.Hashable as Hashable | 161 | import Data.ByteString.Base64 as Base64 |
82 | import qualified Data.List as L | 162 | import Data.ByteString.Char8 as BC (pack, unpack) |
83 | import Data.Text as T | 163 | import Data.ByteString.Lazy as BL |
84 | import Data.Time | 164 | import Data.Char |
165 | import Data.Convertible | ||
166 | import Data.Default | ||
167 | import Data.Foldable as F | ||
168 | import Data.Hashable as Hashable | ||
169 | import Data.Int | ||
170 | import Data.List as L | ||
171 | import Data.Map as M | ||
172 | import Data.Maybe | ||
173 | import Data.Serialize as S | ||
174 | import Data.String | ||
175 | import Data.Text as T | ||
176 | import Data.Text.Encoding as T | ||
177 | import Data.Text.Read | ||
85 | import Data.Time.Clock.POSIX | 178 | import Data.Time.Clock.POSIX |
86 | import Data.Typeable | 179 | import Data.Typeable |
87 | import Network (HostName) | 180 | import Network (HostName) |
181 | import Network.HTTP.Types.QueryLike | ||
182 | import Network.HTTP.Types.URI | ||
88 | import Network.URI | 183 | import Network.URI |
184 | import Text.ParserCombinators.ReadP as P | ||
89 | import Text.PrettyPrint as PP | 185 | import Text.PrettyPrint as PP |
90 | import Text.PrettyPrint.Class | 186 | import Text.PrettyPrint.Class |
91 | import System.FilePath | 187 | import System.FilePath |
188 | import System.Posix.Types | ||
189 | |||
190 | import Network.BitTorrent.Address | ||
191 | |||
192 | |||
193 | {----------------------------------------------------------------------- | ||
194 | -- Info hash | ||
195 | -----------------------------------------------------------------------} | ||
196 | -- TODO | ||
197 | -- | ||
198 | -- data Word160 = Word160 {-# UNPACK #-} !Word64 | ||
199 | -- {-# UNPACK #-} !Word64 | ||
200 | -- {-# UNPACK #-} !Word32 | ||
201 | -- | ||
202 | -- newtype InfoHash = InfoHash Word160 | ||
203 | -- | ||
204 | -- reason: bytestring have overhead = 8 words, while infohash have length 20 bytes | ||
205 | |||
206 | -- $infohash | ||
207 | -- | ||
208 | -- Infohash is a unique identifier of torrent. | ||
209 | |||
210 | -- | Exactly 20 bytes long SHA1 hash of the info part of torrent file. | ||
211 | newtype InfoHash = InfoHash { getInfoHash :: BS.ByteString } | ||
212 | deriving (Eq, Ord, Typeable) | ||
213 | |||
214 | infoHashLen :: Int | ||
215 | infoHashLen = 20 | ||
216 | |||
217 | -- | Meaningless placeholder value. | ||
218 | instance Default InfoHash where | ||
219 | def = "0123456789012345678901234567890123456789" | ||
220 | |||
221 | -- | Hash raw bytes. (no encoding) | ||
222 | instance Hashable InfoHash where | ||
223 | hashWithSalt s (InfoHash ih) = hashWithSalt s ih | ||
224 | {-# INLINE hashWithSalt #-} | ||
225 | |||
226 | -- | Convert to\/from raw bencoded string. (no encoding) | ||
227 | instance BEncode InfoHash where | ||
228 | toBEncode = toBEncode . getInfoHash | ||
229 | fromBEncode be = InfoHash <$> fromBEncode be | ||
230 | |||
231 | -- | Convert to\/from raw bytestring. (no encoding) | ||
232 | instance Serialize InfoHash where | ||
233 | put (InfoHash ih) = putByteString ih | ||
234 | {-# INLINE put #-} | ||
235 | |||
236 | get = InfoHash <$> getBytes infoHashLen | ||
237 | {-# INLINE get #-} | ||
238 | |||
239 | -- | Convert to raw query value. (no encoding) | ||
240 | instance QueryValueLike InfoHash where | ||
241 | toQueryValue (InfoHash ih) = Just ih | ||
242 | {-# INLINE toQueryValue #-} | ||
243 | |||
244 | -- | Convert to base16 encoded string. | ||
245 | instance Show InfoHash where | ||
246 | show (InfoHash ih) = BC.unpack (Base16.encode ih) | ||
247 | |||
248 | -- | Convert to base16 encoded Doc string. | ||
249 | instance Pretty InfoHash where | ||
250 | pretty = text . show | ||
251 | |||
252 | -- | Read base16 encoded string. | ||
253 | instance Read InfoHash where | ||
254 | readsPrec _ = readP_to_S $ do | ||
255 | str <- replicateM (infoHashLen * 2) (satisfy isHexDigit) | ||
256 | return $ InfoHash $ decodeIH str | ||
257 | where | ||
258 | decodeIH = BS.pack . L.map fromHex . pair | ||
259 | fromHex (a, b) = read $ '0' : 'x' : a : b : [] | ||
260 | |||
261 | pair (a : b : xs) = (a, b) : pair xs | ||
262 | pair _ = [] | ||
263 | |||
264 | -- | Convert raw bytes to info hash. | ||
265 | instance Convertible BS.ByteString InfoHash where | ||
266 | safeConvert bs | ||
267 | | BS.length bs == infoHashLen = pure (InfoHash bs) | ||
268 | | otherwise = convError "invalid length" bs | ||
269 | |||
270 | -- | Parse infohash from base16\/base32\/base64 encoded string. | ||
271 | instance Convertible Text InfoHash where | ||
272 | safeConvert t | ||
273 | | 20 == hashLen = pure (InfoHash hashStr) | ||
274 | | 26 <= hashLen && hashLen <= 28 = | ||
275 | case Base64.decode hashStr of | ||
276 | Left msg -> convError ("invalid base64 encoding " ++ msg) t | ||
277 | Right ihStr -> safeConvert ihStr | ||
278 | |||
279 | | hashLen == 32 = | ||
280 | case Base32.decode hashStr of | ||
281 | Left msg -> convError msg t | ||
282 | Right ihStr -> safeConvert ihStr | ||
283 | |||
284 | | hashLen == 40 = | ||
285 | let (ihStr, inv) = Base16.decode hashStr | ||
286 | in if BS.length inv /= 0 | ||
287 | then convError "invalid base16 encoding" t | ||
288 | else safeConvert ihStr | ||
289 | |||
290 | | otherwise = convError "invalid length" t | ||
291 | where | ||
292 | hashLen = BS.length hashStr | ||
293 | hashStr = T.encodeUtf8 t | ||
294 | |||
295 | -- | Decode from base16\/base32\/base64 encoded string. | ||
296 | instance IsString InfoHash where | ||
297 | fromString = either (error . prettyConvertError) id . safeConvert . T.pack | ||
298 | |||
299 | ignoreErrorMsg :: Either a b -> Maybe b | ||
300 | ignoreErrorMsg = either (const Nothing) Just | ||
301 | |||
302 | -- | Tries both base16 and base32 while decoding info hash. | ||
303 | -- | ||
304 | -- Use 'safeConvert' for detailed error messages. | ||
305 | -- | ||
306 | textToInfoHash :: Text -> Maybe InfoHash | ||
307 | textToInfoHash = ignoreErrorMsg . safeConvert | ||
92 | 308 | ||
93 | import Data.Torrent.InfoHash as IH | 309 | -- | Hex encode infohash to text, full length. |
94 | import Data.Torrent.Layout | 310 | longHex :: InfoHash -> Text |
95 | import Data.Torrent.Piece | 311 | longHex = T.decodeUtf8 . Base16.encode . getInfoHash |
96 | import Network.BitTorrent.Core.NodeInfo | 312 | |
313 | -- | The same as 'longHex', but only first 7 characters. | ||
314 | shortHex :: InfoHash -> Text | ||
315 | shortHex = T.take 7 . longHex | ||
316 | |||
317 | {----------------------------------------------------------------------- | ||
318 | -- File info | ||
319 | -----------------------------------------------------------------------} | ||
320 | |||
321 | -- | Size of a file in bytes. | ||
322 | type FileSize = FileOffset | ||
323 | |||
324 | deriving instance BEncode FileOffset | ||
325 | |||
326 | -- | Contain metainfo about one single file. | ||
327 | data FileInfo a = FileInfo { | ||
328 | fiLength :: {-# UNPACK #-} !FileSize | ||
329 | -- ^ Length of the file in bytes. | ||
330 | |||
331 | -- TODO unpacked MD5 sum | ||
332 | , fiMD5Sum :: !(Maybe BS.ByteString) | ||
333 | -- ^ 32 character long MD5 sum of the file. Used by third-party | ||
334 | -- tools, not by bittorrent protocol itself. | ||
335 | |||
336 | , fiName :: !a | ||
337 | -- ^ One or more string elements that together represent the | ||
338 | -- path and filename. Each element in the list corresponds to | ||
339 | -- either a directory name or (in the case of the last element) | ||
340 | -- the filename. For example, the file: | ||
341 | -- | ||
342 | -- > "dir1/dir2/file.ext" | ||
343 | -- | ||
344 | -- would consist of three string elements: | ||
345 | -- | ||
346 | -- > ["dir1", "dir2", "file.ext"] | ||
347 | -- | ||
348 | } deriving (Show, Read, Eq, Typeable | ||
349 | , Functor, Foldable | ||
350 | ) | ||
351 | |||
352 | makeLensesFor | ||
353 | [ ("fiLength", "fileLength") | ||
354 | , ("fiMD5Sum", "fileMD5Sum") | ||
355 | , ("fiName" , "filePath" ) | ||
356 | ] | ||
357 | ''FileInfo | ||
358 | |||
359 | instance NFData a => NFData (FileInfo a) where | ||
360 | rnf FileInfo {..} = rnf fiName | ||
361 | {-# INLINE rnf #-} | ||
362 | |||
363 | instance BEncode (FileInfo [BS.ByteString]) where | ||
364 | toBEncode FileInfo {..} = toDict $ | ||
365 | "length" .=! fiLength | ||
366 | .: "md5sum" .=? fiMD5Sum | ||
367 | .: "path" .=! fiName | ||
368 | .: endDict | ||
369 | {-# INLINE toBEncode #-} | ||
370 | |||
371 | fromBEncode = fromDict $ do | ||
372 | FileInfo <$>! "length" | ||
373 | <*>? "md5sum" | ||
374 | <*>! "path" | ||
375 | {-# INLINE fromBEncode #-} | ||
376 | |||
377 | type Put a = a -> BDict -> BDict | ||
378 | |||
379 | putFileInfoSingle :: Data.Torrent.Put (FileInfo BS.ByteString) | ||
380 | putFileInfoSingle FileInfo {..} cont = | ||
381 | "length" .=! fiLength | ||
382 | .: "md5sum" .=? fiMD5Sum | ||
383 | .: "name" .=! fiName | ||
384 | .: cont | ||
385 | |||
386 | getFileInfoSingle :: BE.Get (FileInfo BS.ByteString) | ||
387 | getFileInfoSingle = do | ||
388 | FileInfo <$>! "length" | ||
389 | <*>? "md5sum" | ||
390 | <*>! "name" | ||
391 | |||
392 | instance BEncode (FileInfo BS.ByteString) where | ||
393 | toBEncode = toDict . (`putFileInfoSingle` endDict) | ||
394 | {-# INLINE toBEncode #-} | ||
395 | |||
396 | fromBEncode = fromDict getFileInfoSingle | ||
397 | {-# INLINE fromBEncode #-} | ||
398 | |||
399 | instance Pretty (FileInfo BS.ByteString) where | ||
400 | pretty FileInfo {..} = | ||
401 | "Path: " <> text (T.unpack (T.decodeUtf8 fiName)) | ||
402 | $$ "Size: " <> text (show fiLength) | ||
403 | $$ maybe PP.empty ppMD5 fiMD5Sum | ||
404 | where | ||
405 | ppMD5 md5 = "MD5 : " <> text (show (Base16.encode md5)) | ||
406 | |||
407 | -- | Join file path. | ||
408 | joinFilePath :: FileInfo [BS.ByteString] -> FileInfo BS.ByteString | ||
409 | joinFilePath = fmap (BS.intercalate "/") | ||
410 | |||
411 | {----------------------------------------------------------------------- | ||
412 | -- Layout info | ||
413 | -----------------------------------------------------------------------} | ||
414 | |||
415 | -- | Original (found in torrent file) layout info is either: | ||
416 | -- | ||
417 | -- * Single file with its /name/. | ||
418 | -- | ||
419 | -- * Multiple files with its relative file /paths/. | ||
420 | -- | ||
421 | data LayoutInfo | ||
422 | = SingleFile | ||
423 | { -- | Single file info. | ||
424 | liFile :: !(FileInfo BS.ByteString) | ||
425 | } | ||
426 | | MultiFile | ||
427 | { -- | List of the all files that torrent contains. | ||
428 | liFiles :: ![FileInfo [BS.ByteString]] | ||
429 | |||
430 | -- | The /suggested/ name of the root directory in which to | ||
431 | -- store all the files. | ||
432 | , liDirName :: !BS.ByteString | ||
433 | } deriving (Show, Read, Eq, Typeable) | ||
434 | |||
435 | makeLensesFor | ||
436 | [ ("liFile" , "singleFile" ) | ||
437 | , ("liFiles" , "multiFile" ) | ||
438 | , ("liDirName", "rootDirName") | ||
439 | ] | ||
440 | ''LayoutInfo | ||
441 | |||
442 | instance NFData LayoutInfo where | ||
443 | rnf SingleFile {..} = () | ||
444 | rnf MultiFile {..} = rnf liFiles | ||
445 | |||
446 | -- | Empty multifile layout. | ||
447 | instance Default LayoutInfo where | ||
448 | def = MultiFile [] "" | ||
449 | |||
450 | getLayoutInfo :: BE.Get LayoutInfo | ||
451 | getLayoutInfo = single <|> multi | ||
452 | where | ||
453 | single = SingleFile <$> getFileInfoSingle | ||
454 | multi = MultiFile <$>! "files" <*>! "name" | ||
455 | |||
456 | putLayoutInfo :: Data.Torrent.Put LayoutInfo | ||
457 | putLayoutInfo SingleFile {..} = putFileInfoSingle liFile | ||
458 | putLayoutInfo MultiFile {..} = \ cont -> | ||
459 | "files" .=! liFiles | ||
460 | .: "name" .=! liDirName | ||
461 | .: cont | ||
462 | |||
463 | instance BEncode LayoutInfo where | ||
464 | toBEncode = toDict . (`putLayoutInfo` endDict) | ||
465 | fromBEncode = fromDict getLayoutInfo | ||
466 | |||
467 | instance Pretty LayoutInfo where | ||
468 | pretty SingleFile {..} = pretty liFile | ||
469 | pretty MultiFile {..} = vcat $ L.map (pretty . joinFilePath) liFiles | ||
470 | |||
471 | -- | Test if this is single file torrent. | ||
472 | isSingleFile :: LayoutInfo -> Bool | ||
473 | isSingleFile SingleFile {} = True | ||
474 | isSingleFile _ = False | ||
475 | {-# INLINE isSingleFile #-} | ||
476 | |||
477 | -- | Test if this is multifile torrent. | ||
478 | isMultiFile :: LayoutInfo -> Bool | ||
479 | isMultiFile MultiFile {} = True | ||
480 | isMultiFile _ = False | ||
481 | {-# INLINE isMultiFile #-} | ||
482 | |||
483 | -- | Get name of the torrent based on the root path piece. | ||
484 | suggestedName :: LayoutInfo -> BS.ByteString | ||
485 | suggestedName (SingleFile FileInfo {..}) = fiName | ||
486 | suggestedName MultiFile {..} = liDirName | ||
487 | {-# INLINE suggestedName #-} | ||
488 | |||
489 | -- | Find sum of sizes of the all torrent files. | ||
490 | contentLength :: LayoutInfo -> FileSize | ||
491 | contentLength SingleFile { liFile = FileInfo {..} } = fiLength | ||
492 | contentLength MultiFile { liFiles = tfs } = L.sum (L.map fiLength tfs) | ||
493 | |||
494 | -- | Get number of all files in torrent. | ||
495 | fileCount :: LayoutInfo -> Int | ||
496 | fileCount SingleFile {..} = 1 | ||
497 | fileCount MultiFile {..} = L.length liFiles | ||
498 | |||
499 | -- | Find number of blocks of the specified size. If torrent size is | ||
500 | -- not a multiple of block size then the count is rounded up. | ||
501 | blockCount :: Int -> LayoutInfo -> Int | ||
502 | blockCount blkSize ci = contentLength ci `sizeInBase` blkSize | ||
503 | |||
504 | ------------------------------------------------------------------------ | ||
505 | |||
506 | -- | File layout specifies the order and the size of each file in the | ||
507 | -- storage. Note that order of files is highly important since we | ||
508 | -- coalesce all the files in the given order to get the linear block | ||
509 | -- address space. | ||
510 | -- | ||
511 | type FileLayout a = [(FilePath, a)] | ||
512 | |||
513 | -- | Extract files layout from torrent info with the given root path. | ||
514 | flatLayout | ||
515 | :: FilePath -- ^ Root path for the all torrent files. | ||
516 | -> LayoutInfo -- ^ Torrent content information. | ||
517 | -> FileLayout FileSize -- ^ The all file paths prefixed with the given root. | ||
518 | flatLayout prefixPath SingleFile { liFile = FileInfo {..} } | ||
519 | = [(prefixPath </> BC.unpack fiName, fiLength)] | ||
520 | flatLayout prefixPath MultiFile {..} = L.map mkPath liFiles | ||
521 | where -- TODO use utf8 encoding in name | ||
522 | mkPath FileInfo {..} = (_path, fiLength) | ||
523 | where | ||
524 | _path = prefixPath </> BC.unpack liDirName | ||
525 | </> joinPath (L.map BC.unpack fiName) | ||
526 | |||
527 | -- | Calculate offset of each file based on its length, incrementally. | ||
528 | accumPositions :: FileLayout FileSize -> FileLayout (FileOffset, FileSize) | ||
529 | accumPositions = go 0 | ||
530 | where | ||
531 | go !_ [] = [] | ||
532 | go !offset ((n, s) : xs) = (n, (offset, s)) : go (offset + s) xs | ||
533 | |||
534 | -- | Gives global offset of a content file for a given full path. | ||
535 | fileOffset :: FilePath -> FileLayout FileOffset -> Maybe FileOffset | ||
536 | fileOffset = L.lookup | ||
537 | {-# INLINE fileOffset #-} | ||
538 | |||
539 | ------------------------------------------------------------------------ | ||
540 | |||
541 | -- | Divide and round up. | ||
542 | sizeInBase :: Integral a => a -> Int -> Int | ||
543 | sizeInBase n b = fromIntegral (n `div` fromIntegral b) + align | ||
544 | where | ||
545 | align = if n `mod` fromIntegral b == 0 then 0 else 1 | ||
546 | {-# SPECIALIZE sizeInBase :: Int -> Int -> Int #-} | ||
547 | {-# SPECIALIZE sizeInBase :: Integer -> Int -> Int #-} | ||
548 | |||
549 | {----------------------------------------------------------------------- | ||
550 | -- Piece attributes | ||
551 | -----------------------------------------------------------------------} | ||
552 | |||
553 | -- | Zero-based index of piece in torrent content. | ||
554 | type PieceIx = Int | ||
555 | |||
556 | -- | Size of piece in bytes. Should be a power of 2. | ||
557 | -- | ||
558 | -- NOTE: Have max and min size constrained to wide used | ||
559 | -- semi-standard values. This bounds should be used to make decision | ||
560 | -- about piece size for new torrents. | ||
561 | -- | ||
562 | type PieceSize = Int | ||
563 | |||
564 | -- | Number of pieces in torrent or a part of torrent. | ||
565 | type PieceCount = Int | ||
566 | |||
567 | defaultBlockSize :: Int | ||
568 | defaultBlockSize = 16 * 1024 | ||
569 | |||
570 | -- | Optimal number of pieces in torrent. | ||
571 | optimalPieceCount :: PieceCount | ||
572 | optimalPieceCount = 1000 | ||
573 | {-# INLINE optimalPieceCount #-} | ||
574 | |||
575 | -- | Piece size should not be less than this value. | ||
576 | minPieceSize :: Int | ||
577 | minPieceSize = defaultBlockSize * 4 | ||
578 | {-# INLINE minPieceSize #-} | ||
579 | |||
580 | -- | To prevent transfer degradation piece size should not exceed this | ||
581 | -- value. | ||
582 | maxPieceSize :: Int | ||
583 | maxPieceSize = 4 * 1024 * 1024 | ||
584 | {-# INLINE maxPieceSize #-} | ||
585 | |||
586 | toPow2 :: Int -> Int | ||
587 | toPow2 x = bit $ fromIntegral (leadingZeros (0 :: Int) - leadingZeros x) | ||
588 | |||
589 | -- | Find the optimal piece size for a given torrent size. | ||
590 | defaultPieceSize :: Int64 -> Int | ||
591 | defaultPieceSize x = max minPieceSize $ min maxPieceSize $ toPow2 pc | ||
592 | where | ||
593 | pc = fromIntegral (x `div` fromIntegral optimalPieceCount) | ||
594 | |||
595 | {----------------------------------------------------------------------- | ||
596 | -- Piece data | ||
597 | -----------------------------------------------------------------------} | ||
598 | |||
599 | type PieceHash = BS.ByteString | ||
600 | |||
601 | hashsize :: Int | ||
602 | hashsize = 20 | ||
603 | {-# INLINE hashsize #-} | ||
604 | |||
605 | -- TODO check if pieceLength is power of 2 | ||
606 | -- | Piece payload should be strict or lazy bytestring. | ||
607 | data Piece a = Piece | ||
608 | { -- | Zero-based piece index in torrent. | ||
609 | pieceIndex :: {-# UNPACK #-} !PieceIx | ||
610 | |||
611 | -- | Payload. | ||
612 | , pieceData :: !a | ||
613 | } deriving (Show, Read, Eq, Functor, Typeable) | ||
614 | |||
615 | instance NFData (Piece a) | ||
616 | |||
617 | -- | Payload bytes are omitted. | ||
618 | instance Pretty (Piece a) where | ||
619 | pretty Piece {..} = "Piece" <+> braces ("index" <+> "=" <+> int pieceIndex) | ||
620 | |||
621 | -- | Get size of piece in bytes. | ||
622 | pieceSize :: Piece BL.ByteString -> PieceSize | ||
623 | pieceSize Piece {..} = fromIntegral (BL.length pieceData) | ||
624 | |||
625 | -- | Get piece hash. | ||
626 | hashPiece :: Piece BL.ByteString -> PieceHash | ||
627 | hashPiece Piece {..} = SHA1.hashlazy pieceData | ||
628 | |||
629 | {----------------------------------------------------------------------- | ||
630 | -- Piece control | ||
631 | -----------------------------------------------------------------------} | ||
632 | |||
633 | -- | A flat array of SHA1 hash for each piece. | ||
634 | newtype HashList = HashList { unHashList :: BS.ByteString } | ||
635 | deriving (Show, Read, Eq, BEncode, Typeable) | ||
636 | |||
637 | -- | Empty hash list. | ||
638 | instance Default HashList where | ||
639 | def = HashList "" | ||
640 | |||
641 | -- | Part of torrent file used for torrent content validation. | ||
642 | data PieceInfo = PieceInfo | ||
643 | { piPieceLength :: {-# UNPACK #-} !PieceSize | ||
644 | -- ^ Number of bytes in each piece. | ||
645 | |||
646 | , piPieceHashes :: !HashList | ||
647 | -- ^ Concatenation of all 20-byte SHA1 hash values. | ||
648 | } deriving (Show, Read, Eq, Typeable) | ||
649 | |||
650 | -- | Number of bytes in each piece. | ||
651 | makeLensesFor [("piPieceLength", "pieceLength")] ''PieceInfo | ||
652 | |||
653 | -- | Concatenation of all 20-byte SHA1 hash values. | ||
654 | makeLensesFor [("piPieceHashes", "pieceHashes")] ''PieceInfo | ||
655 | |||
656 | instance NFData PieceInfo | ||
657 | |||
658 | instance Default PieceInfo where | ||
659 | def = PieceInfo 1 def | ||
660 | |||
661 | class Lint a where | ||
662 | lint :: a -> Either String a | ||
663 | |||
664 | instance Lint PieceInfo where | ||
665 | lint pinfo @ PieceInfo {..} | ||
666 | | BS.length (unHashList piPieceHashes) `rem` hashsize == 0 | ||
667 | , piPieceLength >= 0 = return pinfo | ||
668 | | otherwise = Left undefined | ||
669 | |||
670 | |||
671 | putPieceInfo :: Data.Torrent.Put PieceInfo | ||
672 | putPieceInfo PieceInfo {..} cont = | ||
673 | "piece length" .=! piPieceLength | ||
674 | .: "pieces" .=! piPieceHashes | ||
675 | .: cont | ||
676 | |||
677 | getPieceInfo :: BE.Get PieceInfo | ||
678 | getPieceInfo = do | ||
679 | PieceInfo <$>! "piece length" | ||
680 | <*>! "pieces" | ||
681 | |||
682 | instance BEncode PieceInfo where | ||
683 | toBEncode = toDict . (`putPieceInfo` endDict) | ||
684 | fromBEncode = fromDict getPieceInfo | ||
685 | |||
686 | -- | Hashes are omitted. | ||
687 | instance Pretty PieceInfo where | ||
688 | pretty PieceInfo {..} = "Piece size: " <> int piPieceLength | ||
689 | |||
690 | slice :: Int -> Int -> BS.ByteString -> BS.ByteString | ||
691 | slice start len = BS.take len . BS.drop start | ||
692 | {-# INLINE slice #-} | ||
693 | |||
694 | -- | Extract validation hash by specified piece index. | ||
695 | pieceHash :: PieceInfo -> PieceIx -> PieceHash | ||
696 | pieceHash PieceInfo {..} i = slice (hashsize * i) hashsize (unHashList piPieceHashes) | ||
697 | |||
698 | -- | Find count of pieces in the torrent. If torrent size is not a | ||
699 | -- multiple of piece size then the count is rounded up. | ||
700 | pieceCount :: PieceInfo -> PieceCount | ||
701 | pieceCount PieceInfo {..} = BS.length (unHashList piPieceHashes) `quot` hashsize | ||
702 | |||
703 | -- | Test if this is last piece in torrent content. | ||
704 | isLastPiece :: PieceInfo -> PieceIx -> Bool | ||
705 | isLastPiece ci i = pieceCount ci == succ i | ||
706 | |||
707 | -- | Validate piece with metainfo hash. | ||
708 | checkPieceLazy :: PieceInfo -> Piece BL.ByteString -> Bool | ||
709 | checkPieceLazy pinfo @ PieceInfo {..} Piece {..} | ||
710 | = (fromIntegral (BL.length pieceData) == piPieceLength | ||
711 | || isLastPiece pinfo pieceIndex) | ||
712 | && SHA1.hashlazy pieceData == pieceHash pinfo pieceIndex | ||
97 | 713 | ||
98 | {----------------------------------------------------------------------- | 714 | {----------------------------------------------------------------------- |
99 | -- Info dictionary | 715 | -- Info dictionary |
@@ -145,9 +761,9 @@ instance Default InfoDict where | |||
145 | infoDictionary :: LayoutInfo -> PieceInfo -> Bool -> InfoDict | 761 | infoDictionary :: LayoutInfo -> PieceInfo -> Bool -> InfoDict |
146 | infoDictionary li pinfo private = InfoDict ih li pinfo private | 762 | infoDictionary li pinfo private = InfoDict ih li pinfo private |
147 | where | 763 | where |
148 | ih = hashLazyIH $ encode $ InfoDict def li pinfo private | 764 | ih = hashLazyIH $ BE.encode $ InfoDict def li pinfo private |
149 | 765 | ||
150 | getPrivate :: Get Bool | 766 | getPrivate :: BE.Get Bool |
151 | getPrivate = (Just True ==) <$>? "private" | 767 | getPrivate = (Just True ==) <$>? "private" |
152 | 768 | ||
153 | putPrivate :: Bool -> BDict -> BDict | 769 | putPrivate :: Bool -> BDict -> BDict |
@@ -156,7 +772,7 @@ putPrivate True = \ cont -> "private" .=! True .: cont | |||
156 | 772 | ||
157 | -- | Hash lazy bytestring using SHA1 algorithm. | 773 | -- | Hash lazy bytestring using SHA1 algorithm. |
158 | hashLazyIH :: BL.ByteString -> InfoHash | 774 | hashLazyIH :: BL.ByteString -> InfoHash |
159 | hashLazyIH = either (const (error msg)) id . safeConvert . C.hashlazy | 775 | hashLazyIH = either (const (error msg)) id . safeConvert . SHA1.hashlazy |
160 | where | 776 | where |
161 | msg = "Infohash.hash: impossible: SHA1 is always 20 bytes long" | 777 | msg = "Infohash.hash: impossible: SHA1 is always 20 bytes long" |
162 | 778 | ||
@@ -172,7 +788,7 @@ instance BEncode InfoDict where | |||
172 | <*> getPieceInfo | 788 | <*> getPieceInfo |
173 | <*> getPrivate | 789 | <*> getPrivate |
174 | where | 790 | where |
175 | ih = hashLazyIH (encode dict) | 791 | ih = hashLazyIH (BE.encode dict) |
176 | 792 | ||
177 | ppPrivacy :: Bool -> Doc | 793 | ppPrivacy :: Bool -> Doc |
178 | ppPrivacy privacy = "Privacy: " <> if privacy then "private" else "public" | 794 | ppPrivacy privacy = "Privacy: " <> if privacy then "private" else "public" |
@@ -189,6 +805,7 @@ instance Pretty InfoDict where | |||
189 | {----------------------------------------------------------------------- | 805 | {----------------------------------------------------------------------- |
190 | -- Torrent info | 806 | -- Torrent info |
191 | -----------------------------------------------------------------------} | 807 | -----------------------------------------------------------------------} |
808 | -- TODO add torrent file validation | ||
192 | 809 | ||
193 | -- | Metainfo about particular torrent. | 810 | -- | Metainfo about particular torrent. |
194 | data Torrent = Torrent | 811 | data Torrent = Torrent |
@@ -219,7 +836,7 @@ data Torrent = Torrent | |||
219 | , tNodes :: !(Maybe [NodeAddr HostName]) | 836 | , tNodes :: !(Maybe [NodeAddr HostName]) |
220 | -- ^ This key should be set to the /K closest/ nodes in the | 837 | -- ^ This key should be set to the /K closest/ nodes in the |
221 | -- torrent generating client's routing table. Alternatively, the | 838 | -- torrent generating client's routing table. Alternatively, the |
222 | -- key could be set to a known good 'Network.BitTorrent.Core.Node' | 839 | -- key could be set to a known good 'Network.BitTorrent.Address.Node' |
223 | -- such as one operated by the person generating the torrent. | 840 | -- such as one operated by the person generating the torrent. |
224 | -- | 841 | -- |
225 | -- Please do not automatically add \"router.bittorrent.com\" to | 842 | -- Please do not automatically add \"router.bittorrent.com\" to |
@@ -232,7 +849,7 @@ data Torrent = Torrent | |||
232 | -- authority to allow new peers onto the swarm. | 849 | -- authority to allow new peers onto the swarm. |
233 | 850 | ||
234 | , tPublisherURL :: !(Maybe URI) | 851 | , tPublisherURL :: !(Maybe URI) |
235 | , tSignature :: !(Maybe ByteString) | 852 | , tSignature :: !(Maybe BS.ByteString) |
236 | -- ^ The RSA signature of the info dictionary (specifically, the | 853 | -- ^ The RSA signature of the info dictionary (specifically, the |
237 | -- encrypted SHA-1 hash of the info dictionary). | 854 | -- encrypted SHA-1 hash of the info dictionary). |
238 | } deriving (Show, Eq, Typeable) | 855 | } deriving (Show, Eq, Typeable) |
@@ -361,10 +978,314 @@ isTorrentPath filepath = takeExtension filepath == extSeparator : torrentExt | |||
361 | fromFile :: FilePath -> IO Torrent | 978 | fromFile :: FilePath -> IO Torrent |
362 | fromFile filepath = do | 979 | fromFile filepath = do |
363 | contents <- BS.readFile filepath | 980 | contents <- BS.readFile filepath |
364 | case decode contents of | 981 | case BE.decode contents of |
365 | Right !t -> return t | 982 | Right !t -> return t |
366 | Left msg -> throwIO $ userError $ msg ++ " while reading torrent file" | 983 | Left msg -> throwIO $ userError $ msg ++ " while reading torrent file" |
367 | 984 | ||
368 | -- | Encode and write a .torrent file. | 985 | -- | Encode and write a .torrent file. |
369 | toFile :: FilePath -> Torrent -> IO () | 986 | toFile :: FilePath -> Torrent -> IO () |
370 | toFile filepath = BL.writeFile filepath . encode | 987 | toFile filepath = BL.writeFile filepath . BE.encode |
988 | |||
989 | {----------------------------------------------------------------------- | ||
990 | -- URN | ||
991 | -----------------------------------------------------------------------} | ||
992 | |||
993 | -- | Namespace identifier determines the syntactic interpretation of | ||
994 | -- namespace-specific string. | ||
995 | type NamespaceId = [Text] | ||
996 | |||
997 | -- | BitTorrent Info Hash (hence the name) namespace | ||
998 | -- identifier. Namespace-specific string /should/ be a base16\/base32 | ||
999 | -- encoded SHA1 hash of the corresponding torrent /info/ dictionary. | ||
1000 | -- | ||
1001 | btih :: NamespaceId | ||
1002 | btih = ["btih"] | ||
1003 | |||
1004 | -- | URN is pesistent location-independent identifier for | ||
1005 | -- resources. In particular, URNs are used represent torrent names | ||
1006 | -- as a part of magnet link, see 'Data.Torrent.Magnet.Magnet' for | ||
1007 | -- more info. | ||
1008 | -- | ||
1009 | data URN = URN | ||
1010 | { urnNamespace :: NamespaceId -- ^ a namespace identifier; | ||
1011 | , urnString :: Text -- ^ a corresponding | ||
1012 | -- namespace-specific string. | ||
1013 | } deriving (Eq, Ord, Typeable) | ||
1014 | |||
1015 | ----------------------------------------------------------------------- | ||
1016 | |||
1017 | instance Convertible URN InfoHash where | ||
1018 | safeConvert u @ URN {..} | ||
1019 | | urnNamespace /= btih = convError "invalid namespace" u | ||
1020 | | otherwise = safeConvert urnString | ||
1021 | |||
1022 | -- | Make resource name for torrent with corresponding | ||
1023 | -- infohash. Infohash is base16 (hex) encoded. | ||
1024 | -- | ||
1025 | infohashURN :: InfoHash -> URN | ||
1026 | infohashURN = URN btih . longHex | ||
1027 | |||
1028 | -- | Meaningless placeholder value. | ||
1029 | instance Default URN where | ||
1030 | def = infohashURN def | ||
1031 | |||
1032 | ------------------------------------------------------------------------ | ||
1033 | |||
1034 | -- | Render URN to its text representation. | ||
1035 | renderURN :: URN -> Text | ||
1036 | renderURN URN {..} | ||
1037 | = T.intercalate ":" $ "urn" : urnNamespace ++ [urnString] | ||
1038 | |||
1039 | instance Pretty URN where | ||
1040 | pretty = text . T.unpack . renderURN | ||
1041 | |||
1042 | instance Show URN where | ||
1043 | showsPrec n = showsPrec n . T.unpack . renderURN | ||
1044 | |||
1045 | instance QueryValueLike URN where | ||
1046 | toQueryValue = toQueryValue . renderURN | ||
1047 | {-# INLINE toQueryValue #-} | ||
1048 | |||
1049 | ----------------------------------------------------------------------- | ||
1050 | |||
1051 | _unsnoc :: [a] -> Maybe ([a], a) | ||
1052 | _unsnoc [] = Nothing | ||
1053 | _unsnoc xs = Just (L.init xs, L.last xs) | ||
1054 | |||
1055 | instance Convertible Text URN where | ||
1056 | safeConvert t = case T.split (== ':') t of | ||
1057 | uriScheme : body | ||
1058 | | T.toLower uriScheme == "urn" -> | ||
1059 | case _unsnoc body of | ||
1060 | Just (namespace, val) -> pure URN | ||
1061 | { urnNamespace = namespace | ||
1062 | , urnString = val | ||
1063 | } | ||
1064 | Nothing -> convError "missing URN string" body | ||
1065 | | otherwise -> convError "invalid URN scheme" uriScheme | ||
1066 | [] -> convError "missing URN scheme" t | ||
1067 | |||
1068 | instance IsString URN where | ||
1069 | fromString = either (error . prettyConvertError) id | ||
1070 | . safeConvert . T.pack | ||
1071 | |||
1072 | -- | Try to parse an URN from its text representation. | ||
1073 | -- | ||
1074 | -- Use 'safeConvert' for detailed error messages. | ||
1075 | -- | ||
1076 | parseURN :: Text -> Maybe URN | ||
1077 | parseURN = either (const Nothing) pure . safeConvert | ||
1078 | |||
1079 | {----------------------------------------------------------------------- | ||
1080 | -- Magnet | ||
1081 | -----------------------------------------------------------------------} | ||
1082 | -- $magnet-link | ||
1083 | -- | ||
1084 | -- Magnet URI scheme is an standard defining Magnet links. Magnet | ||
1085 | -- links are refer to resources by hash, in particular magnet links | ||
1086 | -- can refer to torrent using corresponding infohash. In this way, | ||
1087 | -- magnet links can be used instead of torrent files. | ||
1088 | -- | ||
1089 | -- This module provides bittorrent specific implementation of magnet | ||
1090 | -- links. | ||
1091 | -- | ||
1092 | -- For generic magnet uri scheme see: | ||
1093 | -- <http://magnet-uri.sourceforge.net/magnet-draft-overview.txt>, | ||
1094 | -- <http://www.iana.org/assignments/uri-schemes/prov/magnet> | ||
1095 | -- | ||
1096 | -- Bittorrent specific details: | ||
1097 | -- <http://www.bittorrent.org/beps/bep_0009.html> | ||
1098 | -- | ||
1099 | |||
1100 | -- TODO multiple exact topics | ||
1101 | -- TODO render/parse supplement for URI/query | ||
1102 | |||
1103 | -- | An URI used to identify torrent. | ||
1104 | data Magnet = Magnet | ||
1105 | { -- | Torrent infohash hash. Can be used in DHT queries if no | ||
1106 | -- 'tracker' provided. | ||
1107 | exactTopic :: !InfoHash -- TODO InfoHash -> URN? | ||
1108 | |||
1109 | -- | A filename for the file to download. Can be used to | ||
1110 | -- display name while waiting for metadata. | ||
1111 | , displayName :: Maybe Text | ||
1112 | |||
1113 | -- | Size of the resource in bytes. | ||
1114 | , exactLength :: Maybe Integer | ||
1115 | |||
1116 | -- | URI pointing to manifest, e.g. a list of further items. | ||
1117 | , manifest :: Maybe Text | ||
1118 | |||
1119 | -- | Search string. | ||
1120 | , keywordTopic :: Maybe Text | ||
1121 | |||
1122 | -- | A source to be queried after not being able to find and | ||
1123 | -- download the file in the bittorrent network in a defined | ||
1124 | -- amount of time. | ||
1125 | , acceptableSource :: Maybe URI | ||
1126 | |||
1127 | -- | Direct link to the resource. | ||
1128 | , exactSource :: Maybe URI | ||
1129 | |||
1130 | -- | URI to the tracker. | ||
1131 | , tracker :: Maybe URI | ||
1132 | |||
1133 | -- | Additional or experimental parameters. | ||
1134 | , supplement :: Map Text Text | ||
1135 | } deriving (Eq, Ord, Typeable) | ||
1136 | |||
1137 | instance QueryValueLike Integer where | ||
1138 | toQueryValue = toQueryValue . show | ||
1139 | |||
1140 | instance QueryValueLike URI where | ||
1141 | toQueryValue = toQueryValue . show | ||
1142 | |||
1143 | instance QueryLike Magnet where | ||
1144 | toQuery Magnet {..} = | ||
1145 | [ ("xt", toQueryValue $ infohashURN exactTopic) | ||
1146 | , ("dn", toQueryValue displayName) | ||
1147 | , ("xl", toQueryValue exactLength) | ||
1148 | , ("mt", toQueryValue manifest) | ||
1149 | , ("kt", toQueryValue keywordTopic) | ||
1150 | , ("as", toQueryValue acceptableSource) | ||
1151 | , ("xs", toQueryValue exactSource) | ||
1152 | , ("tr", toQueryValue tracker) | ||
1153 | ] | ||
1154 | |||
1155 | instance QueryValueLike Magnet where | ||
1156 | toQueryValue = toQueryValue . renderMagnet | ||
1157 | |||
1158 | instance Convertible QueryText Magnet where | ||
1159 | safeConvert xs = do | ||
1160 | urnStr <- getTextMsg "xt" "exact topic not defined" xs | ||
1161 | infoHash <- convertVia (error "safeConvert" :: URN) urnStr | ||
1162 | return Magnet | ||
1163 | { exactTopic = infoHash | ||
1164 | , displayName = getText "dn" xs | ||
1165 | , exactLength = getText "xl" xs >>= getInt | ||
1166 | , manifest = getText "mt" xs | ||
1167 | , keywordTopic = getText "kt" xs | ||
1168 | , acceptableSource = getText "as" xs >>= getURI | ||
1169 | , exactSource = getText "xs" xs >>= getURI | ||
1170 | , tracker = getText "tr" xs >>= getURI | ||
1171 | , supplement = M.empty | ||
1172 | } | ||
1173 | where | ||
1174 | getInt = either (const Nothing) (Just . fst) . signed decimal | ||
1175 | getURI = parseURI . T.unpack | ||
1176 | getText p = join . L.lookup p | ||
1177 | getTextMsg p msg ps = maybe (convError msg xs) pure $ getText p ps | ||
1178 | |||
1179 | magnetScheme :: URI | ||
1180 | magnetScheme = URI | ||
1181 | { uriScheme = "magnet:" | ||
1182 | , uriAuthority = Nothing | ||
1183 | , uriPath = "" | ||
1184 | , uriQuery = "" | ||
1185 | , uriFragment = "" | ||
1186 | } | ||
1187 | |||
1188 | isMagnetURI :: URI -> Bool | ||
1189 | isMagnetURI u = u { uriQuery = "" } == magnetScheme | ||
1190 | |||
1191 | -- | Can be used instead of 'parseMagnet'. | ||
1192 | instance Convertible URI Magnet where | ||
1193 | safeConvert u @ URI {..} | ||
1194 | | not (isMagnetURI u) = convError "this is not a magnet link" u | ||
1195 | | otherwise = safeConvert $ parseQueryText $ BC.pack uriQuery | ||
1196 | |||
1197 | -- | Can be used instead of 'renderMagnet'. | ||
1198 | instance Convertible Magnet URI where | ||
1199 | safeConvert m = pure $ magnetScheme | ||
1200 | { uriQuery = BC.unpack $ renderQuery True $ toQuery m } | ||
1201 | |||
1202 | instance Convertible String Magnet where | ||
1203 | safeConvert str | ||
1204 | | Just uri <- parseURI str = safeConvert uri | ||
1205 | | otherwise = convError "unable to parse uri" str | ||
1206 | |||
1207 | ------------------------------------------------------------------------ | ||
1208 | |||
1209 | -- | Meaningless placeholder value. | ||
1210 | instance Default Magnet where | ||
1211 | def = Magnet | ||
1212 | { exactTopic = def | ||
1213 | , displayName = Nothing | ||
1214 | , exactLength = Nothing | ||
1215 | , manifest = Nothing | ||
1216 | , keywordTopic = Nothing | ||
1217 | , acceptableSource = Nothing | ||
1218 | , exactSource = Nothing | ||
1219 | , tracker = Nothing | ||
1220 | , supplement = M.empty | ||
1221 | } | ||
1222 | |||
1223 | -- | Set 'exactTopic' ('xt' param) only, other params are empty. | ||
1224 | nullMagnet :: InfoHash -> Magnet | ||
1225 | nullMagnet u = Magnet | ||
1226 | { exactTopic = u | ||
1227 | , displayName = Nothing | ||
1228 | , exactLength = Nothing | ||
1229 | , manifest = Nothing | ||
1230 | , keywordTopic = Nothing | ||
1231 | , acceptableSource = Nothing | ||
1232 | , exactSource = Nothing | ||
1233 | , tracker = Nothing | ||
1234 | , supplement = M.empty | ||
1235 | } | ||
1236 | |||
1237 | -- | Like 'nullMagnet' but also include 'displayName' ('dn' param). | ||
1238 | simpleMagnet :: Torrent -> Magnet | ||
1239 | simpleMagnet Torrent {tInfoDict = InfoDict {..}} | ||
1240 | = (nullMagnet idInfoHash) | ||
1241 | { displayName = Just $ T.decodeUtf8 $ suggestedName idLayoutInfo | ||
1242 | } | ||
1243 | |||
1244 | -- | Like 'simpleMagnet' but also include 'exactLength' ('xl' param) and | ||
1245 | -- 'tracker' ('tr' param). | ||
1246 | -- | ||
1247 | detailedMagnet :: Torrent -> Magnet | ||
1248 | detailedMagnet t @ Torrent {tInfoDict = InfoDict {..}, tAnnounce} | ||
1249 | = (simpleMagnet t) | ||
1250 | { exactLength = Just $ fromIntegral $ contentLength idLayoutInfo | ||
1251 | , tracker = tAnnounce | ||
1252 | } | ||
1253 | |||
1254 | ----------------------------------------------------------------------- | ||
1255 | |||
1256 | parseMagnetStr :: String -> Maybe Magnet | ||
1257 | parseMagnetStr = either (const Nothing) Just . safeConvert | ||
1258 | |||
1259 | renderMagnetStr :: Magnet -> String | ||
1260 | renderMagnetStr = show . (convert :: Magnet -> URI) | ||
1261 | |||
1262 | instance Pretty Magnet where | ||
1263 | pretty = PP.text . renderMagnetStr | ||
1264 | |||
1265 | instance Show Magnet where | ||
1266 | show = renderMagnetStr | ||
1267 | {-# INLINE show #-} | ||
1268 | |||
1269 | instance Read Magnet where | ||
1270 | readsPrec _ xs | ||
1271 | | Just m <- parseMagnetStr mstr = [(m, rest)] | ||
1272 | | otherwise = [] | ||
1273 | where | ||
1274 | (mstr, rest) = L.break (== ' ') xs | ||
1275 | |||
1276 | instance IsString Magnet where | ||
1277 | fromString str = fromMaybe (error msg) $ parseMagnetStr str | ||
1278 | where | ||
1279 | msg = "unable to parse magnet: " ++ str | ||
1280 | |||
1281 | -- | Try to parse magnet link from urlencoded string. Use | ||
1282 | -- 'safeConvert' to find out error location. | ||
1283 | -- | ||
1284 | parseMagnet :: Text -> Maybe Magnet | ||
1285 | parseMagnet = parseMagnetStr . T.unpack | ||
1286 | {-# INLINE parseMagnet #-} | ||
1287 | |||
1288 | -- | Render magnet link to urlencoded string | ||
1289 | renderMagnet :: Magnet -> Text | ||
1290 | renderMagnet = T.pack . renderMagnetStr | ||
1291 | {-# INLINE renderMagnet #-} | ||
diff --git a/src/Data/Torrent/Bitfield.hs b/src/Data/Torrent/Bitfield.hs deleted file mode 100644 index b65f058b..00000000 --- a/src/Data/Torrent/Bitfield.hs +++ /dev/null | |||
@@ -1,324 +0,0 @@ | |||
1 | -- | | ||
2 | -- Copyright : (c) Sam Truzjan 2013 | ||
3 | -- License : BSD3 | ||
4 | -- Maintainer : pxqr.sta@gmail.com | ||
5 | -- Stability : experimental | ||
6 | -- Portability : portable | ||
7 | -- | ||
8 | -- This modules provides all necessary machinery to work with | ||
9 | -- bitfields. Bitfields are used to keep track indices of complete | ||
10 | -- pieces either peer have or client have. | ||
11 | -- | ||
12 | -- There are also commonly used piece seletion algorithms | ||
13 | -- which used to find out which one next piece to download. | ||
14 | -- Selectors considered to be used in the following order: | ||
15 | -- | ||
16 | -- * Random first - at the start. | ||
17 | -- | ||
18 | -- * Rarest first selection - performed to avoid situation when | ||
19 | -- rarest piece is unaccessible. | ||
20 | -- | ||
21 | -- * /End game/ seletion - performed after a peer has requested all | ||
22 | -- the subpieces of the content. | ||
23 | -- | ||
24 | -- Note that BitTorrent applies the strict priority policy for | ||
25 | -- /subpiece/ or /blocks/ selection. | ||
26 | -- | ||
27 | {-# LANGUAGE CPP #-} | ||
28 | {-# LANGUAGE BangPatterns #-} | ||
29 | {-# LANGUAGE RecordWildCards #-} | ||
30 | module Data.Torrent.Bitfield | ||
31 | ( -- * Bitfield | ||
32 | PieceIx | ||
33 | , PieceCount | ||
34 | , Bitfield | ||
35 | |||
36 | -- * Construction | ||
37 | , haveAll | ||
38 | , haveNone | ||
39 | , have | ||
40 | , singleton | ||
41 | , interval | ||
42 | , adjustSize | ||
43 | |||
44 | -- * Query | ||
45 | -- ** Cardinality | ||
46 | , Data.Torrent.Bitfield.null | ||
47 | , Data.Torrent.Bitfield.full | ||
48 | , haveCount | ||
49 | , totalCount | ||
50 | , completeness | ||
51 | |||
52 | -- ** Membership | ||
53 | , member | ||
54 | , notMember | ||
55 | , findMin | ||
56 | , findMax | ||
57 | , isSubsetOf | ||
58 | |||
59 | -- ** Availability | ||
60 | , complement | ||
61 | , Frequency | ||
62 | , frequencies | ||
63 | , rarest | ||
64 | |||
65 | -- * Combine | ||
66 | , insert | ||
67 | , union | ||
68 | , intersection | ||
69 | , difference | ||
70 | |||
71 | -- * Conversion | ||
72 | , toList | ||
73 | , fromList | ||
74 | |||
75 | -- * Serialization | ||
76 | , fromBitmap | ||
77 | , toBitmap | ||
78 | ) where | ||
79 | |||
80 | import Control.Monad | ||
81 | import Control.Monad.ST | ||
82 | import Data.ByteString (ByteString) | ||
83 | import qualified Data.ByteString as B | ||
84 | import qualified Data.ByteString.Lazy as Lazy | ||
85 | import Data.Vector.Unboxed (Vector) | ||
86 | import qualified Data.Vector.Unboxed as V | ||
87 | import qualified Data.Vector.Unboxed.Mutable as VM | ||
88 | import Data.IntervalSet (IntSet) | ||
89 | import qualified Data.IntervalSet as S | ||
90 | import qualified Data.IntervalSet.ByteString as S | ||
91 | import Data.List (foldl') | ||
92 | import Data.Monoid | ||
93 | import Data.Ratio | ||
94 | |||
95 | import Data.Torrent.Piece | ||
96 | |||
97 | -- TODO cache some operations | ||
98 | |||
99 | -- | Bitfields are represented just as integer sets but with | ||
100 | -- restriction: the each set should be within given interval (or | ||
101 | -- subset of the specified interval). Size is used to specify | ||
102 | -- interval, so bitfield of size 10 might contain only indices in | ||
103 | -- interval [0..9]. | ||
104 | -- | ||
105 | data Bitfield = Bitfield { | ||
106 | bfSize :: !PieceCount | ||
107 | , bfSet :: !IntSet | ||
108 | } deriving (Show, Read, Eq) | ||
109 | |||
110 | -- Invariants: all elements of bfSet lie in [0..bfSize - 1]; | ||
111 | |||
112 | instance Monoid Bitfield where | ||
113 | {-# SPECIALIZE instance Monoid Bitfield #-} | ||
114 | mempty = haveNone 0 | ||
115 | mappend = union | ||
116 | mconcat = unions | ||
117 | |||
118 | {----------------------------------------------------------------------- | ||
119 | Construction | ||
120 | -----------------------------------------------------------------------} | ||
121 | |||
122 | -- | The empty bitfield of the given size. | ||
123 | haveNone :: PieceCount -> Bitfield | ||
124 | haveNone s = Bitfield s S.empty | ||
125 | |||
126 | -- | The full bitfield containing all piece indices for the given size. | ||
127 | haveAll :: PieceCount -> Bitfield | ||
128 | haveAll s = Bitfield s (S.interval 0 (s - 1)) | ||
129 | |||
130 | -- | Insert the index in the set ignoring out of range indices. | ||
131 | have :: PieceIx -> Bitfield -> Bitfield | ||
132 | have ix Bitfield {..} | ||
133 | | 0 <= ix && ix < bfSize = Bitfield bfSize (S.insert ix bfSet) | ||
134 | | otherwise = Bitfield bfSize bfSet | ||
135 | |||
136 | singleton :: PieceIx -> PieceCount -> Bitfield | ||
137 | singleton ix pc = have ix (haveNone pc) | ||
138 | |||
139 | -- | Assign new size to bitfield. FIXME Normally, size should be only | ||
140 | -- decreased, otherwise exception raised. | ||
141 | adjustSize :: PieceCount -> Bitfield -> Bitfield | ||
142 | adjustSize s Bitfield {..} = Bitfield s bfSet | ||
143 | |||
144 | -- | NOTE: for internal use only | ||
145 | interval :: PieceCount -> PieceIx -> PieceIx -> Bitfield | ||
146 | interval pc a b = Bitfield pc (S.interval a b) | ||
147 | |||
148 | {----------------------------------------------------------------------- | ||
149 | Query | ||
150 | -----------------------------------------------------------------------} | ||
151 | |||
152 | -- | Test if bitifield have no one index: peer do not have anything. | ||
153 | null :: Bitfield -> Bool | ||
154 | null Bitfield {..} = S.null bfSet | ||
155 | |||
156 | -- | Test if bitfield have all pieces. | ||
157 | full :: Bitfield -> Bool | ||
158 | full Bitfield {..} = S.size bfSet == bfSize | ||
159 | |||
160 | -- | Count of peer have pieces. | ||
161 | haveCount :: Bitfield -> PieceCount | ||
162 | haveCount = S.size . bfSet | ||
163 | |||
164 | -- | Total count of pieces and its indices. | ||
165 | totalCount :: Bitfield -> PieceCount | ||
166 | totalCount = bfSize | ||
167 | |||
168 | -- | Ratio of /have/ piece count to the /total/ piece count. | ||
169 | -- | ||
170 | -- > forall bf. 0 <= completeness bf <= 1 | ||
171 | -- | ||
172 | completeness :: Bitfield -> Ratio PieceCount | ||
173 | completeness b = haveCount b % totalCount b | ||
174 | |||
175 | inRange :: PieceIx -> Bitfield -> Bool | ||
176 | inRange ix Bitfield {..} = 0 <= ix && ix < bfSize | ||
177 | |||
178 | member :: PieceIx -> Bitfield -> Bool | ||
179 | member ix bf @ Bitfield {..} | ||
180 | | ix `inRange` bf = ix `S.member` bfSet | ||
181 | | otherwise = False | ||
182 | |||
183 | notMember :: PieceIx -> Bitfield -> Bool | ||
184 | notMember ix bf @ Bitfield {..} | ||
185 | | ix `inRange` bf = ix `S.notMember` bfSet | ||
186 | | otherwise = True | ||
187 | |||
188 | -- | Find first available piece index. | ||
189 | findMin :: Bitfield -> PieceIx | ||
190 | findMin = S.findMin . bfSet | ||
191 | {-# INLINE findMin #-} | ||
192 | |||
193 | -- | Find last available piece index. | ||
194 | findMax :: Bitfield -> PieceIx | ||
195 | findMax = S.findMax . bfSet | ||
196 | {-# INLINE findMax #-} | ||
197 | |||
198 | -- | Check if all pieces from first bitfield present if the second bitfield | ||
199 | isSubsetOf :: Bitfield -> Bitfield -> Bool | ||
200 | isSubsetOf a b = bfSet a `S.isSubsetOf` bfSet b | ||
201 | {-# INLINE isSubsetOf #-} | ||
202 | |||
203 | -- | Resulting bitfield includes only missing pieces. | ||
204 | complement :: Bitfield -> Bitfield | ||
205 | complement Bitfield {..} = Bitfield | ||
206 | { bfSet = uni `S.difference` bfSet | ||
207 | , bfSize = bfSize | ||
208 | } | ||
209 | where | ||
210 | Bitfield _ uni = haveAll bfSize | ||
211 | {-# INLINE complement #-} | ||
212 | |||
213 | {----------------------------------------------------------------------- | ||
214 | -- Availability | ||
215 | -----------------------------------------------------------------------} | ||
216 | |||
217 | -- | Frequencies are needed in piece selection startegies which use | ||
218 | -- availability quantity to find out the optimal next piece index to | ||
219 | -- download. | ||
220 | type Frequency = Int | ||
221 | |||
222 | -- TODO rename to availability | ||
223 | -- | How many times each piece index occur in the given bitfield set. | ||
224 | frequencies :: [Bitfield] -> Vector Frequency | ||
225 | frequencies [] = V.fromList [] | ||
226 | frequencies xs = runST $ do | ||
227 | v <- VM.new size | ||
228 | VM.set v 0 | ||
229 | forM_ xs $ \ Bitfield {..} -> do | ||
230 | forM_ (S.toList bfSet) $ \ x -> do | ||
231 | fr <- VM.read v x | ||
232 | VM.write v x (succ fr) | ||
233 | V.unsafeFreeze v | ||
234 | where | ||
235 | size = maximum (map bfSize xs) | ||
236 | |||
237 | -- TODO it seems like this operation is veeery slow | ||
238 | |||
239 | -- | Find least available piece index. If no piece available return | ||
240 | -- 'Nothing'. | ||
241 | rarest :: [Bitfield] -> Maybe PieceIx | ||
242 | rarest xs | ||
243 | | V.null freqMap = Nothing | ||
244 | | otherwise | ||
245 | = Just $ fst $ V.ifoldr' minIx (0, freqMap V.! 0) freqMap | ||
246 | where | ||
247 | freqMap = frequencies xs | ||
248 | |||
249 | minIx :: PieceIx -> Frequency | ||
250 | -> (PieceIx, Frequency) | ||
251 | -> (PieceIx, Frequency) | ||
252 | minIx ix fr acc@(_, fra) | ||
253 | | fr < fra && fr > 0 = (ix, fr) | ||
254 | | otherwise = acc | ||
255 | |||
256 | |||
257 | {----------------------------------------------------------------------- | ||
258 | Combine | ||
259 | -----------------------------------------------------------------------} | ||
260 | |||
261 | insert :: PieceIx -> Bitfield -> Bitfield | ||
262 | insert pix bf @ Bitfield {..} | ||
263 | | 0 <= pix && pix < bfSize = Bitfield | ||
264 | { bfSet = S.insert pix bfSet | ||
265 | , bfSize = bfSize | ||
266 | } | ||
267 | | otherwise = bf | ||
268 | |||
269 | -- | Find indices at least one peer have. | ||
270 | union :: Bitfield -> Bitfield -> Bitfield | ||
271 | union a b = {-# SCC union #-} Bitfield { | ||
272 | bfSize = bfSize a `max` bfSize b | ||
273 | , bfSet = bfSet a `S.union` bfSet b | ||
274 | } | ||
275 | |||
276 | -- | Find indices both peers have. | ||
277 | intersection :: Bitfield -> Bitfield -> Bitfield | ||
278 | intersection a b = {-# SCC intersection #-} Bitfield { | ||
279 | bfSize = bfSize a `min` bfSize b | ||
280 | , bfSet = bfSet a `S.intersection` bfSet b | ||
281 | } | ||
282 | |||
283 | -- | Find indices which have first peer but do not have the second peer. | ||
284 | difference :: Bitfield -> Bitfield -> Bitfield | ||
285 | difference a b = {-# SCC difference #-} Bitfield { | ||
286 | bfSize = bfSize a -- FIXME is it reasonable? | ||
287 | , bfSet = bfSet a `S.difference` bfSet b | ||
288 | } | ||
289 | |||
290 | -- | Find indices the any of the peers have. | ||
291 | unions :: [Bitfield] -> Bitfield | ||
292 | unions = {-# SCC unions #-} foldl' union (haveNone 0) | ||
293 | |||
294 | {----------------------------------------------------------------------- | ||
295 | Serialization | ||
296 | -----------------------------------------------------------------------} | ||
297 | |||
298 | -- | List all /have/ indexes. | ||
299 | toList :: Bitfield -> [PieceIx] | ||
300 | toList Bitfield {..} = S.toList bfSet | ||
301 | |||
302 | -- | Make bitfield from list of /have/ indexes. | ||
303 | fromList :: PieceCount -> [PieceIx] -> Bitfield | ||
304 | fromList s ixs = Bitfield { | ||
305 | bfSize = s | ||
306 | , bfSet = S.splitGT (-1) $ S.splitLT s $ S.fromList ixs | ||
307 | } | ||
308 | |||
309 | -- | Unpack 'Bitfield' from tightly packed bit array. Note resulting | ||
310 | -- size might be more than real bitfield size, use 'adjustSize'. | ||
311 | fromBitmap :: ByteString -> Bitfield | ||
312 | fromBitmap bs = {-# SCC fromBitmap #-} Bitfield { | ||
313 | bfSize = B.length bs * 8 | ||
314 | , bfSet = S.fromByteString bs | ||
315 | } | ||
316 | {-# INLINE fromBitmap #-} | ||
317 | |||
318 | -- | Pack a 'Bitfield' to tightly packed bit array. | ||
319 | toBitmap :: Bitfield -> Lazy.ByteString | ||
320 | toBitmap Bitfield {..} = {-# SCC toBitmap #-} Lazy.fromChunks [intsetBM, alignment] | ||
321 | where | ||
322 | byteSize = bfSize `div` 8 + if bfSize `mod` 8 == 0 then 0 else 1 | ||
323 | alignment = B.replicate (byteSize - B.length intsetBM) 0 | ||
324 | intsetBM = S.toByteString bfSet | ||
diff --git a/src/Data/Torrent/InfoHash.hs b/src/Data/Torrent/InfoHash.hs deleted file mode 100644 index f322ac6f..00000000 --- a/src/Data/Torrent/InfoHash.hs +++ /dev/null | |||
@@ -1,164 +0,0 @@ | |||
1 | -- | | ||
2 | -- Copyright : (c) Sam Truzjan 2013 | ||
3 | -- License : BSD3 | ||
4 | -- Maintainer : pxqr.sta@gmail.com | ||
5 | -- Stability : provisional | ||
6 | -- Portability : portable | ||
7 | -- | ||
8 | -- Infohash is a unique identifier of torrent. | ||
9 | -- | ||
10 | {-# LANGUAGE FlexibleInstances #-} | ||
11 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} | ||
12 | {-# LANGUAGE MultiParamTypeClasses #-} | ||
13 | {-# LANGUAGE DeriveDataTypeable #-} | ||
14 | module Data.Torrent.InfoHash | ||
15 | ( InfoHash | ||
16 | |||
17 | -- * Parsing | ||
18 | , textToInfoHash | ||
19 | |||
20 | -- * Rendering | ||
21 | , longHex | ||
22 | , shortHex | ||
23 | ) where | ||
24 | |||
25 | import Control.Applicative | ||
26 | import Control.Monad | ||
27 | import Data.BEncode | ||
28 | import Data.ByteString as BS | ||
29 | import Data.ByteString.Char8 as BC | ||
30 | import Data.ByteString.Base16 as Base16 | ||
31 | import Data.ByteString.Base32 as Base32 | ||
32 | import Data.ByteString.Base64 as Base64 | ||
33 | import Data.Char | ||
34 | import Data.Convertible.Base | ||
35 | import Data.Default | ||
36 | import Data.List as L | ||
37 | import Data.Hashable as Hashable | ||
38 | import Data.Serialize | ||
39 | import Data.String | ||
40 | import Data.Text as T | ||
41 | import Data.Text.Encoding as T | ||
42 | import Data.Typeable | ||
43 | import Network.HTTP.Types.QueryLike | ||
44 | import Text.ParserCombinators.ReadP as P | ||
45 | import Text.PrettyPrint | ||
46 | import Text.PrettyPrint.Class | ||
47 | |||
48 | |||
49 | -- TODO | ||
50 | -- | ||
51 | -- data Word160 = Word160 {-# UNPACK #-} !Word64 | ||
52 | -- {-# UNPACK #-} !Word64 | ||
53 | -- {-# UNPACK #-} !Word32 | ||
54 | -- | ||
55 | -- newtype InfoHash = InfoHash Word160 | ||
56 | -- | ||
57 | -- reason: bytestring have overhead = 8 words, while infohash have length 20 bytes | ||
58 | |||
59 | -- | Exactly 20 bytes long SHA1 hash of the info part of torrent file. | ||
60 | newtype InfoHash = InfoHash { getInfoHash :: BS.ByteString } | ||
61 | deriving (Eq, Ord, Typeable) | ||
62 | |||
63 | infoHashLen :: Int | ||
64 | infoHashLen = 20 | ||
65 | |||
66 | -- | Meaningless placeholder value. | ||
67 | instance Default InfoHash where | ||
68 | def = "0123456789012345678901234567890123456789" | ||
69 | |||
70 | -- | Hash raw bytes. (no encoding) | ||
71 | instance Hashable InfoHash where | ||
72 | hashWithSalt s (InfoHash ih) = hashWithSalt s ih | ||
73 | {-# INLINE hashWithSalt #-} | ||
74 | |||
75 | -- | Convert to\/from raw bencoded string. (no encoding) | ||
76 | instance BEncode InfoHash where | ||
77 | toBEncode = toBEncode . getInfoHash | ||
78 | fromBEncode be = InfoHash <$> fromBEncode be | ||
79 | |||
80 | -- | Convert to\/from raw bytestring. (no encoding) | ||
81 | instance Serialize InfoHash where | ||
82 | put (InfoHash ih) = putByteString ih | ||
83 | {-# INLINE put #-} | ||
84 | |||
85 | get = InfoHash <$> getBytes infoHashLen | ||
86 | {-# INLINE get #-} | ||
87 | |||
88 | -- | Convert to raw query value. (no encoding) | ||
89 | instance QueryValueLike InfoHash where | ||
90 | toQueryValue (InfoHash ih) = Just ih | ||
91 | {-# INLINE toQueryValue #-} | ||
92 | |||
93 | -- | Convert to base16 encoded string. | ||
94 | instance Show InfoHash where | ||
95 | show (InfoHash ih) = BC.unpack (Base16.encode ih) | ||
96 | |||
97 | -- | Convert to base16 encoded Doc string. | ||
98 | instance Pretty InfoHash where | ||
99 | pretty = text . show | ||
100 | |||
101 | -- | Read base16 encoded string. | ||
102 | instance Read InfoHash where | ||
103 | readsPrec _ = readP_to_S $ do | ||
104 | str <- replicateM (infoHashLen * 2) (satisfy isHexDigit) | ||
105 | return $ InfoHash $ decodeIH str | ||
106 | where | ||
107 | decodeIH = BS.pack . L.map fromHex . pair | ||
108 | fromHex (a, b) = read $ '0' : 'x' : a : b : [] | ||
109 | |||
110 | pair (a : b : xs) = (a, b) : pair xs | ||
111 | pair _ = [] | ||
112 | |||
113 | -- | Convert raw bytes to info hash. | ||
114 | instance Convertible BS.ByteString InfoHash where | ||
115 | safeConvert bs | ||
116 | | BS.length bs == infoHashLen = pure (InfoHash bs) | ||
117 | | otherwise = convError "invalid length" bs | ||
118 | |||
119 | -- | Parse infohash from base16\/base32\/base64 encoded string. | ||
120 | instance Convertible Text InfoHash where | ||
121 | safeConvert t | ||
122 | | 20 == hashLen = pure (InfoHash hashStr) | ||
123 | | 26 <= hashLen && hashLen <= 28 = | ||
124 | case Base64.decode hashStr of | ||
125 | Left msg -> convError ("invalid base64 encoding " ++ msg) t | ||
126 | Right ihStr -> safeConvert ihStr | ||
127 | |||
128 | | hashLen == 32 = | ||
129 | case Base32.decode hashStr of | ||
130 | Left msg -> convError msg t | ||
131 | Right ihStr -> safeConvert ihStr | ||
132 | |||
133 | | hashLen == 40 = | ||
134 | let (ihStr, inv) = Base16.decode hashStr | ||
135 | in if BS.length inv /= 0 | ||
136 | then convError "invalid base16 encoding" t | ||
137 | else safeConvert ihStr | ||
138 | |||
139 | | otherwise = convError "invalid length" t | ||
140 | where | ||
141 | hashLen = BS.length hashStr | ||
142 | hashStr = T.encodeUtf8 t | ||
143 | |||
144 | -- | Decode from base16\/base32\/base64 encoded string. | ||
145 | instance IsString InfoHash where | ||
146 | fromString = either (error . prettyConvertError) id . safeConvert . T.pack | ||
147 | |||
148 | ignoreErrorMsg :: Either a b -> Maybe b | ||
149 | ignoreErrorMsg = either (const Nothing) Just | ||
150 | |||
151 | -- | Tries both base16 and base32 while decoding info hash. | ||
152 | -- | ||
153 | -- Use 'safeConvert' for detailed error messages. | ||
154 | -- | ||
155 | textToInfoHash :: Text -> Maybe InfoHash | ||
156 | textToInfoHash = ignoreErrorMsg . safeConvert | ||
157 | |||
158 | -- | Hex encode infohash to text, full length. | ||
159 | longHex :: InfoHash -> Text | ||
160 | longHex = T.decodeUtf8 . Base16.encode . getInfoHash | ||
161 | |||
162 | -- | The same as 'longHex', but only first 7 characters. | ||
163 | shortHex :: InfoHash -> Text | ||
164 | shortHex = T.take 7 . longHex | ||
diff --git a/src/Data/Torrent/Layout.hs b/src/Data/Torrent/Layout.hs deleted file mode 100644 index cc529840..00000000 --- a/src/Data/Torrent/Layout.hs +++ /dev/null | |||
@@ -1,321 +0,0 @@ | |||
1 | -- | | ||
2 | -- Copyright : (c) Sam Truzjan 2013 | ||
3 | -- License : BSD3 | ||
4 | -- Maintainer : pxqr.sta@gmail.com | ||
5 | -- Stability : experimental | ||
6 | -- Portability : portable | ||
7 | -- | ||
8 | -- Layout of files in torrent. | ||
9 | -- | ||
10 | {-# LANGUAGE BangPatterns #-} | ||
11 | {-# LANGUAGE FlexibleInstances #-} | ||
12 | {-# LANGUAGE StandaloneDeriving #-} | ||
13 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} | ||
14 | {-# LANGUAGE DeriveDataTypeable #-} | ||
15 | {-# LANGUAGE DeriveFunctor #-} | ||
16 | {-# LANGUAGE DeriveFoldable #-} | ||
17 | {-# LANGUAGE DeriveTraversable #-} | ||
18 | {-# LANGUAGE TemplateHaskell #-} | ||
19 | {-# OPTIONS -fno-warn-orphans #-} | ||
20 | module Data.Torrent.Layout | ||
21 | ( -- * File attributes | ||
22 | FileOffset | ||
23 | , FileSize | ||
24 | |||
25 | -- * Single file info | ||
26 | , FileInfo (..) | ||
27 | |||
28 | -- ** Lens | ||
29 | , fileLength | ||
30 | , filePath | ||
31 | , fileMD5Sum | ||
32 | |||
33 | -- * File layout | ||
34 | , LayoutInfo (..) | ||
35 | , joinFilePath | ||
36 | |||
37 | -- ** Lens | ||
38 | , singleFile | ||
39 | , multiFile | ||
40 | , rootDirName | ||
41 | |||
42 | -- ** Predicates | ||
43 | , isSingleFile | ||
44 | , isMultiFile | ||
45 | |||
46 | -- ** Query | ||
47 | , suggestedName | ||
48 | , contentLength | ||
49 | , fileCount | ||
50 | , blockCount | ||
51 | |||
52 | -- * Flat file layout | ||
53 | , FileLayout | ||
54 | , flatLayout | ||
55 | , accumPositions | ||
56 | , fileOffset | ||
57 | |||
58 | -- * Internal | ||
59 | , sizeInBase | ||
60 | , getLayoutInfo | ||
61 | , putLayoutInfo | ||
62 | ) where | ||
63 | |||
64 | import Control.Applicative | ||
65 | import Control.DeepSeq | ||
66 | import Control.Lens | ||
67 | import Data.BEncode | ||
68 | import Data.BEncode.Types | ||
69 | import Data.ByteString as BS | ||
70 | import Data.ByteString.Base16 as Base16 | ||
71 | import Data.ByteString.Char8 as BC | ||
72 | import Data.Default | ||
73 | import Data.Foldable as F | ||
74 | import Data.List as L | ||
75 | import Data.Text as T | ||
76 | import Data.Text.Encoding as T | ||
77 | import Data.Typeable | ||
78 | import Text.PrettyPrint as PP | ||
79 | import Text.PrettyPrint.Class | ||
80 | import System.FilePath | ||
81 | import System.Posix.Types | ||
82 | |||
83 | {----------------------------------------------------------------------- | ||
84 | -- File attribytes | ||
85 | -----------------------------------------------------------------------} | ||
86 | |||
87 | -- | Size of a file in bytes. | ||
88 | type FileSize = FileOffset | ||
89 | |||
90 | deriving instance BEncode FileOffset | ||
91 | |||
92 | {----------------------------------------------------------------------- | ||
93 | -- File info both either from info dict or file list | ||
94 | -----------------------------------------------------------------------} | ||
95 | |||
96 | -- | Contain metainfo about one single file. | ||
97 | data FileInfo a = FileInfo { | ||
98 | fiLength :: {-# UNPACK #-} !FileSize | ||
99 | -- ^ Length of the file in bytes. | ||
100 | |||
101 | -- TODO unpacked MD5 sum | ||
102 | , fiMD5Sum :: !(Maybe ByteString) | ||
103 | -- ^ 32 character long MD5 sum of the file. Used by third-party | ||
104 | -- tools, not by bittorrent protocol itself. | ||
105 | |||
106 | , fiName :: !a | ||
107 | -- ^ One or more string elements that together represent the | ||
108 | -- path and filename. Each element in the list corresponds to | ||
109 | -- either a directory name or (in the case of the last element) | ||
110 | -- the filename. For example, the file: | ||
111 | -- | ||
112 | -- > "dir1/dir2/file.ext" | ||
113 | -- | ||
114 | -- would consist of three string elements: | ||
115 | -- | ||
116 | -- > ["dir1", "dir2", "file.ext"] | ||
117 | -- | ||
118 | } deriving (Show, Read, Eq, Typeable | ||
119 | , Functor, Foldable | ||
120 | ) | ||
121 | |||
122 | makeLensesFor | ||
123 | [ ("fiLength", "fileLength") | ||
124 | , ("fiMD5Sum", "fileMD5Sum") | ||
125 | , ("fiName" , "filePath" ) | ||
126 | ] | ||
127 | ''FileInfo | ||
128 | |||
129 | instance NFData a => NFData (FileInfo a) where | ||
130 | rnf FileInfo {..} = rnf fiName | ||
131 | {-# INLINE rnf #-} | ||
132 | |||
133 | instance BEncode (FileInfo [ByteString]) where | ||
134 | toBEncode FileInfo {..} = toDict $ | ||
135 | "length" .=! fiLength | ||
136 | .: "md5sum" .=? fiMD5Sum | ||
137 | .: "path" .=! fiName | ||
138 | .: endDict | ||
139 | {-# INLINE toBEncode #-} | ||
140 | |||
141 | fromBEncode = fromDict $ do | ||
142 | FileInfo <$>! "length" | ||
143 | <*>? "md5sum" | ||
144 | <*>! "path" | ||
145 | {-# INLINE fromBEncode #-} | ||
146 | |||
147 | type Put a = a -> BDict -> BDict | ||
148 | |||
149 | putFileInfoSingle :: Put (FileInfo ByteString) | ||
150 | putFileInfoSingle FileInfo {..} cont = | ||
151 | "length" .=! fiLength | ||
152 | .: "md5sum" .=? fiMD5Sum | ||
153 | .: "name" .=! fiName | ||
154 | .: cont | ||
155 | |||
156 | getFileInfoSingle :: Get (FileInfo ByteString) | ||
157 | getFileInfoSingle = do | ||
158 | FileInfo <$>! "length" | ||
159 | <*>? "md5sum" | ||
160 | <*>! "name" | ||
161 | |||
162 | instance BEncode (FileInfo ByteString) where | ||
163 | toBEncode = toDict . (`putFileInfoSingle` endDict) | ||
164 | {-# INLINE toBEncode #-} | ||
165 | |||
166 | fromBEncode = fromDict getFileInfoSingle | ||
167 | {-# INLINE fromBEncode #-} | ||
168 | |||
169 | instance Pretty (FileInfo BS.ByteString) where | ||
170 | pretty FileInfo {..} = | ||
171 | "Path: " <> text (T.unpack (T.decodeUtf8 fiName)) | ||
172 | $$ "Size: " <> text (show fiLength) | ||
173 | $$ maybe PP.empty ppMD5 fiMD5Sum | ||
174 | where | ||
175 | ppMD5 md5 = "MD5 : " <> text (show (Base16.encode md5)) | ||
176 | |||
177 | -- | Join file path. | ||
178 | joinFilePath :: FileInfo [ByteString] -> FileInfo ByteString | ||
179 | joinFilePath = fmap (BS.intercalate "/") | ||
180 | |||
181 | {----------------------------------------------------------------------- | ||
182 | -- Original torrent file layout info | ||
183 | -----------------------------------------------------------------------} | ||
184 | |||
185 | -- | Original (found in torrent file) layout info is either: | ||
186 | -- | ||
187 | -- * Single file with its /name/. | ||
188 | -- | ||
189 | -- * Multiple files with its relative file /paths/. | ||
190 | -- | ||
191 | data LayoutInfo | ||
192 | = SingleFile | ||
193 | { -- | Single file info. | ||
194 | liFile :: !(FileInfo ByteString) | ||
195 | } | ||
196 | | MultiFile | ||
197 | { -- | List of the all files that torrent contains. | ||
198 | liFiles :: ![FileInfo [ByteString]] | ||
199 | |||
200 | -- | The /suggested/ name of the root directory in which to | ||
201 | -- store all the files. | ||
202 | , liDirName :: !ByteString | ||
203 | } deriving (Show, Read, Eq, Typeable) | ||
204 | |||
205 | makeLensesFor | ||
206 | [ ("liFile" , "singleFile" ) | ||
207 | , ("liFiles" , "multiFile" ) | ||
208 | , ("liDirName", "rootDirName") | ||
209 | ] | ||
210 | ''LayoutInfo | ||
211 | |||
212 | instance NFData LayoutInfo where | ||
213 | rnf SingleFile {..} = () | ||
214 | rnf MultiFile {..} = rnf liFiles | ||
215 | |||
216 | -- | Empty multifile layout. | ||
217 | instance Default LayoutInfo where | ||
218 | def = MultiFile [] "" | ||
219 | |||
220 | getLayoutInfo :: Get LayoutInfo | ||
221 | getLayoutInfo = single <|> multi | ||
222 | where | ||
223 | single = SingleFile <$> getFileInfoSingle | ||
224 | multi = MultiFile <$>! "files" <*>! "name" | ||
225 | |||
226 | putLayoutInfo :: Put LayoutInfo | ||
227 | putLayoutInfo SingleFile {..} = putFileInfoSingle liFile | ||
228 | putLayoutInfo MultiFile {..} = \ cont -> | ||
229 | "files" .=! liFiles | ||
230 | .: "name" .=! liDirName | ||
231 | .: cont | ||
232 | |||
233 | instance BEncode LayoutInfo where | ||
234 | toBEncode = toDict . (`putLayoutInfo` endDict) | ||
235 | fromBEncode = fromDict getLayoutInfo | ||
236 | |||
237 | instance Pretty LayoutInfo where | ||
238 | pretty SingleFile {..} = pretty liFile | ||
239 | pretty MultiFile {..} = vcat $ L.map (pretty . joinFilePath) liFiles | ||
240 | |||
241 | -- | Test if this is single file torrent. | ||
242 | isSingleFile :: LayoutInfo -> Bool | ||
243 | isSingleFile SingleFile {} = True | ||
244 | isSingleFile _ = False | ||
245 | {-# INLINE isSingleFile #-} | ||
246 | |||
247 | -- | Test if this is multifile torrent. | ||
248 | isMultiFile :: LayoutInfo -> Bool | ||
249 | isMultiFile MultiFile {} = True | ||
250 | isMultiFile _ = False | ||
251 | {-# INLINE isMultiFile #-} | ||
252 | |||
253 | -- | Get name of the torrent based on the root path piece. | ||
254 | suggestedName :: LayoutInfo -> ByteString | ||
255 | suggestedName (SingleFile FileInfo {..}) = fiName | ||
256 | suggestedName MultiFile {..} = liDirName | ||
257 | {-# INLINE suggestedName #-} | ||
258 | |||
259 | -- | Find sum of sizes of the all torrent files. | ||
260 | contentLength :: LayoutInfo -> FileSize | ||
261 | contentLength SingleFile { liFile = FileInfo {..} } = fiLength | ||
262 | contentLength MultiFile { liFiles = tfs } = L.sum (L.map fiLength tfs) | ||
263 | |||
264 | -- | Get number of all files in torrent. | ||
265 | fileCount :: LayoutInfo -> Int | ||
266 | fileCount SingleFile {..} = 1 | ||
267 | fileCount MultiFile {..} = L.length liFiles | ||
268 | |||
269 | -- | Find number of blocks of the specified size. If torrent size is | ||
270 | -- not a multiple of block size then the count is rounded up. | ||
271 | blockCount :: Int -> LayoutInfo -> Int | ||
272 | blockCount blkSize ci = contentLength ci `sizeInBase` blkSize | ||
273 | |||
274 | {----------------------------------------------------------------------- | ||
275 | -- Flat layout | ||
276 | -----------------------------------------------------------------------} | ||
277 | |||
278 | -- | File layout specifies the order and the size of each file in the | ||
279 | -- storage. Note that order of files is highly important since we | ||
280 | -- coalesce all the files in the given order to get the linear block | ||
281 | -- address space. | ||
282 | -- | ||
283 | type FileLayout a = [(FilePath, a)] | ||
284 | |||
285 | -- | Extract files layout from torrent info with the given root path. | ||
286 | flatLayout | ||
287 | :: FilePath -- ^ Root path for the all torrent files. | ||
288 | -> LayoutInfo -- ^ Torrent content information. | ||
289 | -> FileLayout FileSize -- ^ The all file paths prefixed with the given root. | ||
290 | flatLayout prefixPath SingleFile { liFile = FileInfo {..} } | ||
291 | = [(prefixPath </> BC.unpack fiName, fiLength)] | ||
292 | flatLayout prefixPath MultiFile {..} = L.map mkPath liFiles | ||
293 | where -- TODO use utf8 encoding in name | ||
294 | mkPath FileInfo {..} = (path, fiLength) | ||
295 | where | ||
296 | path = prefixPath </> BC.unpack liDirName | ||
297 | </> joinPath (L.map BC.unpack fiName) | ||
298 | |||
299 | -- | Calculate offset of each file based on its length, incrementally. | ||
300 | accumPositions :: FileLayout FileSize -> FileLayout (FileOffset, FileSize) | ||
301 | accumPositions = go 0 | ||
302 | where | ||
303 | go !_ [] = [] | ||
304 | go !offset ((n, s) : xs) = (n, (offset, s)) : go (offset + s) xs | ||
305 | |||
306 | -- | Gives global offset of a content file for a given full path. | ||
307 | fileOffset :: FilePath -> FileLayout FileOffset -> Maybe FileOffset | ||
308 | fileOffset = lookup | ||
309 | {-# INLINE fileOffset #-} | ||
310 | |||
311 | {----------------------------------------------------------------------- | ||
312 | -- Internal utilities | ||
313 | -----------------------------------------------------------------------} | ||
314 | |||
315 | -- | Divide and round up. | ||
316 | sizeInBase :: Integral a => a -> Int -> Int | ||
317 | sizeInBase n b = fromIntegral (n `div` fromIntegral b) + align | ||
318 | where | ||
319 | align = if n `mod` fromIntegral b == 0 then 0 else 1 | ||
320 | {-# SPECIALIZE sizeInBase :: Int -> Int -> Int #-} | ||
321 | {-# SPECIALIZE sizeInBase :: Integer -> Int -> Int #-} | ||
diff --git a/src/Data/Torrent/Magnet.hs b/src/Data/Torrent/Magnet.hs deleted file mode 100644 index aad0debe..00000000 --- a/src/Data/Torrent/Magnet.hs +++ /dev/null | |||
@@ -1,372 +0,0 @@ | |||
1 | -- | | ||
2 | -- Copyright : (c) Sam Truzjan 2013 | ||
3 | -- License : BSD3 | ||
4 | -- Maintainer : pxqr.sta@gmail.com | ||
5 | -- Stability : provisional | ||
6 | -- Portability : portable | ||
7 | -- | ||
8 | -- Magnet URI scheme is an standard defining Magnet links. Magnet | ||
9 | -- links are refer to resources by hash, in particular magnet links | ||
10 | -- can refer to torrent using corresponding infohash. In this way, | ||
11 | -- magnet links can be used instead of torrent files. | ||
12 | -- | ||
13 | -- This module provides bittorrent specific implementation of magnet | ||
14 | -- links. | ||
15 | -- | ||
16 | -- For generic magnet uri scheme see: | ||
17 | -- <http://magnet-uri.sourceforge.net/magnet-draft-overview.txt>, | ||
18 | -- <http://www.iana.org/assignments/uri-schemes/prov/magnet> | ||
19 | -- | ||
20 | -- Bittorrent specific details: | ||
21 | -- <http://www.bittorrent.org/beps/bep_0009.html> | ||
22 | -- | ||
23 | {-# LANGUAGE NamedFieldPuns #-} | ||
24 | {-# LANGUAGE FlexibleInstances #-} | ||
25 | {-# LANGUAGE MultiParamTypeClasses #-} | ||
26 | {-# LANGUAGE TypeSynonymInstances #-} | ||
27 | {-# LANGUAGE DeriveDataTypeable #-} | ||
28 | {-# OPTIONS -fno-warn-orphans #-} | ||
29 | module Data.Torrent.Magnet | ||
30 | ( -- * Magnet | ||
31 | Magnet(..) | ||
32 | |||
33 | -- ** Construction | ||
34 | , nullMagnet | ||
35 | , simpleMagnet | ||
36 | , detailedMagnet | ||
37 | |||
38 | -- ** Conversion | ||
39 | , parseMagnet | ||
40 | , renderMagnet | ||
41 | |||
42 | -- * URN | ||
43 | , URN (..) | ||
44 | |||
45 | -- ** Namespaces | ||
46 | , NamespaceId | ||
47 | , btih | ||
48 | |||
49 | -- ** Construction | ||
50 | , infohashURN | ||
51 | |||
52 | -- ** Conversion | ||
53 | , parseURN | ||
54 | , renderURN | ||
55 | ) where | ||
56 | |||
57 | import Control.Applicative | ||
58 | import Control.Monad | ||
59 | import Data.ByteString.Char8 as BC | ||
60 | import Data.Convertible | ||
61 | import Data.Default | ||
62 | import Data.Map as M | ||
63 | import Data.Maybe | ||
64 | import Data.List as L | ||
65 | import Data.String | ||
66 | import Data.Text as T | ||
67 | import Data.Text.Encoding as T | ||
68 | import Data.Text.Read | ||
69 | import Data.Typeable | ||
70 | import Network.HTTP.Types.QueryLike | ||
71 | import Network.HTTP.Types.URI | ||
72 | import Network.URI | ||
73 | import Text.PrettyPrint as PP | ||
74 | import Text.PrettyPrint.Class | ||
75 | |||
76 | import Data.Torrent | ||
77 | import Data.Torrent.InfoHash | ||
78 | import Data.Torrent.Layout | ||
79 | |||
80 | |||
81 | -- | Namespace identifier determines the syntactic interpretation of | ||
82 | -- namespace-specific string. | ||
83 | type NamespaceId = [Text] | ||
84 | |||
85 | -- | BitTorrent Info Hash (hence the name) namespace | ||
86 | -- identifier. Namespace-specific string /should/ be a base16\/base32 | ||
87 | -- encoded SHA1 hash of the corresponding torrent /info/ dictionary. | ||
88 | -- | ||
89 | btih :: NamespaceId | ||
90 | btih = ["btih"] | ||
91 | |||
92 | -- | URN is pesistent location-independent identifier for | ||
93 | -- resources. In particular, URNs are used represent torrent names | ||
94 | -- as a part of magnet link, see 'Data.Torrent.Magnet.Magnet' for | ||
95 | -- more info. | ||
96 | -- | ||
97 | data URN = URN | ||
98 | { urnNamespace :: NamespaceId -- ^ a namespace identifier; | ||
99 | , urnString :: Text -- ^ a corresponding | ||
100 | -- namespace-specific string. | ||
101 | } deriving (Eq, Ord, Typeable) | ||
102 | |||
103 | {----------------------------------------------------------------------- | ||
104 | -- URN to infohash convertion | ||
105 | -----------------------------------------------------------------------} | ||
106 | |||
107 | instance Convertible URN InfoHash where | ||
108 | safeConvert u @ URN {..} | ||
109 | | urnNamespace /= btih = convError "invalid namespace" u | ||
110 | | otherwise = safeConvert urnString | ||
111 | |||
112 | -- | Make resource name for torrent with corresponding | ||
113 | -- infohash. Infohash is base16 (hex) encoded. | ||
114 | -- | ||
115 | infohashURN :: InfoHash -> URN | ||
116 | infohashURN = URN btih . longHex | ||
117 | |||
118 | -- | Meaningless placeholder value. | ||
119 | instance Default URN where | ||
120 | def = infohashURN def | ||
121 | |||
122 | {----------------------------------------------------------------------- | ||
123 | -- URN Rendering | ||
124 | -----------------------------------------------------------------------} | ||
125 | |||
126 | -- | Render URN to its text representation. | ||
127 | renderURN :: URN -> Text | ||
128 | renderURN URN {..} | ||
129 | = T.intercalate ":" $ "urn" : urnNamespace ++ [urnString] | ||
130 | |||
131 | instance Pretty URN where | ||
132 | pretty = text . T.unpack . renderURN | ||
133 | |||
134 | instance Show URN where | ||
135 | showsPrec n = showsPrec n . T.unpack . renderURN | ||
136 | |||
137 | instance QueryValueLike URN where | ||
138 | toQueryValue = toQueryValue . renderURN | ||
139 | {-# INLINE toQueryValue #-} | ||
140 | |||
141 | {----------------------------------------------------------------------- | ||
142 | -- URN Parsing | ||
143 | -----------------------------------------------------------------------} | ||
144 | |||
145 | unsnoc :: [a] -> Maybe ([a], a) | ||
146 | unsnoc [] = Nothing | ||
147 | unsnoc xs = Just (L.init xs, L.last xs) | ||
148 | |||
149 | instance Convertible Text URN where | ||
150 | safeConvert t = case T.split (== ':') t of | ||
151 | uriScheme : body | ||
152 | | T.toLower uriScheme == "urn" -> | ||
153 | case unsnoc body of | ||
154 | Just (namespace, val) -> pure URN | ||
155 | { urnNamespace = namespace | ||
156 | , urnString = val | ||
157 | } | ||
158 | Nothing -> convError "missing URN string" body | ||
159 | | otherwise -> convError "invalid URN scheme" uriScheme | ||
160 | [] -> convError "missing URN scheme" t | ||
161 | |||
162 | instance IsString URN where | ||
163 | fromString = either (error . prettyConvertError) id | ||
164 | . safeConvert . T.pack | ||
165 | |||
166 | -- | Try to parse an URN from its text representation. | ||
167 | -- | ||
168 | -- Use 'safeConvert' for detailed error messages. | ||
169 | -- | ||
170 | parseURN :: Text -> Maybe URN | ||
171 | parseURN = either (const Nothing) pure . safeConvert | ||
172 | |||
173 | {----------------------------------------------------------------------- | ||
174 | -- Magnet | ||
175 | -----------------------------------------------------------------------} | ||
176 | |||
177 | -- TODO multiple exact topics | ||
178 | -- TODO render/parse supplement for URI/query | ||
179 | |||
180 | -- | An URI used to identify torrent. | ||
181 | data Magnet = Magnet | ||
182 | { -- | Torrent infohash hash. Can be used in DHT queries if no | ||
183 | -- 'tracker' provided. | ||
184 | exactTopic :: !InfoHash -- TODO InfoHash -> URN? | ||
185 | |||
186 | -- | A filename for the file to download. Can be used to | ||
187 | -- display name while waiting for metadata. | ||
188 | , displayName :: Maybe Text | ||
189 | |||
190 | -- | Size of the resource in bytes. | ||
191 | , exactLength :: Maybe Integer | ||
192 | |||
193 | -- | URI pointing to manifest, e.g. a list of further items. | ||
194 | , manifest :: Maybe Text | ||
195 | |||
196 | -- | Search string. | ||
197 | , keywordTopic :: Maybe Text | ||
198 | |||
199 | -- | A source to be queried after not being able to find and | ||
200 | -- download the file in the bittorrent network in a defined | ||
201 | -- amount of time. | ||
202 | , acceptableSource :: Maybe URI | ||
203 | |||
204 | -- | Direct link to the resource. | ||
205 | , exactSource :: Maybe URI | ||
206 | |||
207 | -- | URI to the tracker. | ||
208 | , tracker :: Maybe URI | ||
209 | |||
210 | -- | Additional or experimental parameters. | ||
211 | , supplement :: Map Text Text | ||
212 | } deriving (Eq, Ord, Typeable) | ||
213 | |||
214 | instance QueryValueLike Integer where | ||
215 | toQueryValue = toQueryValue . show | ||
216 | |||
217 | instance QueryValueLike URI where | ||
218 | toQueryValue = toQueryValue . show | ||
219 | |||
220 | instance QueryLike Magnet where | ||
221 | toQuery Magnet {..} = | ||
222 | [ ("xt", toQueryValue $ infohashURN exactTopic) | ||
223 | , ("dn", toQueryValue displayName) | ||
224 | , ("xl", toQueryValue exactLength) | ||
225 | , ("mt", toQueryValue manifest) | ||
226 | , ("kt", toQueryValue keywordTopic) | ||
227 | , ("as", toQueryValue acceptableSource) | ||
228 | , ("xs", toQueryValue exactSource) | ||
229 | , ("tr", toQueryValue tracker) | ||
230 | ] | ||
231 | |||
232 | instance QueryValueLike Magnet where | ||
233 | toQueryValue = toQueryValue . renderMagnet | ||
234 | |||
235 | instance Convertible QueryText Magnet where | ||
236 | safeConvert xs = do | ||
237 | urnStr <- getTextMsg "xt" "exact topic not defined" xs | ||
238 | infoHash <- convertVia (error "safeConvert" :: URN) urnStr | ||
239 | return Magnet | ||
240 | { exactTopic = infoHash | ||
241 | , displayName = getText "dn" xs | ||
242 | , exactLength = getText "xl" xs >>= getInt | ||
243 | , manifest = getText "mt" xs | ||
244 | , keywordTopic = getText "kt" xs | ||
245 | , acceptableSource = getText "as" xs >>= getURI | ||
246 | , exactSource = getText "xs" xs >>= getURI | ||
247 | , tracker = getText "tr" xs >>= getURI | ||
248 | , supplement = M.empty | ||
249 | } | ||
250 | where | ||
251 | getInt = either (const Nothing) (Just . fst) . signed decimal | ||
252 | getURI = parseURI . T.unpack | ||
253 | getText p = join . L.lookup p | ||
254 | getTextMsg p msg ps = maybe (convError msg xs) pure $ getText p ps | ||
255 | |||
256 | magnetScheme :: URI | ||
257 | magnetScheme = URI | ||
258 | { uriScheme = "magnet:" | ||
259 | , uriAuthority = Nothing | ||
260 | , uriPath = "" | ||
261 | , uriQuery = "" | ||
262 | , uriFragment = "" | ||
263 | } | ||
264 | |||
265 | isMagnetURI :: URI -> Bool | ||
266 | isMagnetURI u = u { uriQuery = "" } == magnetScheme | ||
267 | |||
268 | -- | Can be used instead of 'parseMagnet'. | ||
269 | instance Convertible URI Magnet where | ||
270 | safeConvert u @ URI {..} | ||
271 | | not (isMagnetURI u) = convError "this is not a magnet link" u | ||
272 | | otherwise = safeConvert $ parseQueryText $ BC.pack uriQuery | ||
273 | |||
274 | -- | Can be used instead of 'renderMagnet'. | ||
275 | instance Convertible Magnet URI where | ||
276 | safeConvert m = pure $ magnetScheme | ||
277 | { uriQuery = BC.unpack $ renderQuery True $ toQuery m } | ||
278 | |||
279 | instance Convertible String Magnet where | ||
280 | safeConvert str | ||
281 | | Just uri <- parseURI str = safeConvert uri | ||
282 | | otherwise = convError "unable to parse uri" str | ||
283 | |||
284 | {----------------------------------------------------------------------- | ||
285 | -- Magnet Construction | ||
286 | -----------------------------------------------------------------------} | ||
287 | |||
288 | -- | Meaningless placeholder value. | ||
289 | instance Default Magnet where | ||
290 | def = Magnet | ||
291 | { exactTopic = def | ||
292 | , displayName = Nothing | ||
293 | , exactLength = Nothing | ||
294 | , manifest = Nothing | ||
295 | , keywordTopic = Nothing | ||
296 | , acceptableSource = Nothing | ||
297 | , exactSource = Nothing | ||
298 | , tracker = Nothing | ||
299 | , supplement = M.empty | ||
300 | } | ||
301 | |||
302 | -- | Set 'exactTopic' ('xt' param) only, other params are empty. | ||
303 | nullMagnet :: InfoHash -> Magnet | ||
304 | nullMagnet u = Magnet | ||
305 | { exactTopic = u | ||
306 | , displayName = Nothing | ||
307 | , exactLength = Nothing | ||
308 | , manifest = Nothing | ||
309 | , keywordTopic = Nothing | ||
310 | , acceptableSource = Nothing | ||
311 | , exactSource = Nothing | ||
312 | , tracker = Nothing | ||
313 | , supplement = M.empty | ||
314 | } | ||
315 | |||
316 | -- | Like 'nullMagnet' but also include 'displayName' ('dn' param). | ||
317 | simpleMagnet :: Torrent -> Magnet | ||
318 | simpleMagnet Torrent {tInfoDict = InfoDict {..}} | ||
319 | = (nullMagnet idInfoHash) | ||
320 | { displayName = Just $ T.decodeUtf8 $ suggestedName idLayoutInfo | ||
321 | } | ||
322 | |||
323 | -- | Like 'simpleMagnet' but also include 'exactLength' ('xl' param) and | ||
324 | -- 'tracker' ('tr' param). | ||
325 | -- | ||
326 | detailedMagnet :: Torrent -> Magnet | ||
327 | detailedMagnet t @ Torrent {tInfoDict = InfoDict {..}, tAnnounce} | ||
328 | = (simpleMagnet t) | ||
329 | { exactLength = Just $ fromIntegral $ contentLength idLayoutInfo | ||
330 | , tracker = tAnnounce | ||
331 | } | ||
332 | |||
333 | {----------------------------------------------------------------------- | ||
334 | -- Magnet Conversion | ||
335 | -----------------------------------------------------------------------} | ||
336 | |||
337 | parseMagnetStr :: String -> Maybe Magnet | ||
338 | parseMagnetStr = either (const Nothing) Just . safeConvert | ||
339 | |||
340 | renderMagnetStr :: Magnet -> String | ||
341 | renderMagnetStr = show . (convert :: Magnet -> URI) | ||
342 | |||
343 | instance Pretty Magnet where | ||
344 | pretty = PP.text . renderMagnetStr | ||
345 | |||
346 | instance Show Magnet where | ||
347 | show = renderMagnetStr | ||
348 | {-# INLINE show #-} | ||
349 | |||
350 | instance Read Magnet where | ||
351 | readsPrec _ xs | ||
352 | | Just m <- parseMagnetStr mstr = [(m, rest)] | ||
353 | | otherwise = [] | ||
354 | where | ||
355 | (mstr, rest) = L.break (== ' ') xs | ||
356 | |||
357 | instance IsString Magnet where | ||
358 | fromString str = fromMaybe (error msg) $ parseMagnetStr str | ||
359 | where | ||
360 | msg = "unable to parse magnet: " ++ str | ||
361 | |||
362 | -- | Try to parse magnet link from urlencoded string. Use | ||
363 | -- 'safeConvert' to find out error location. | ||
364 | -- | ||
365 | parseMagnet :: Text -> Maybe Magnet | ||
366 | parseMagnet = parseMagnetStr . T.unpack | ||
367 | {-# INLINE parseMagnet #-} | ||
368 | |||
369 | -- | Render magnet link to urlencoded string | ||
370 | renderMagnet :: Magnet -> Text | ||
371 | renderMagnet = T.pack . renderMagnetStr | ||
372 | {-# INLINE renderMagnet #-} | ||
diff --git a/src/Data/Torrent/Piece.hs b/src/Data/Torrent/Piece.hs deleted file mode 100644 index d4b2c399..00000000 --- a/src/Data/Torrent/Piece.hs +++ /dev/null | |||
@@ -1,232 +0,0 @@ | |||
1 | -- | | ||
2 | -- Copyright : (c) Sam Truzjan 2013 | ||
3 | -- License : BSD3 | ||
4 | -- Maintainer : pxqr.sta@gmail.com | ||
5 | -- Stability : experimental | ||
6 | -- Portability : portable | ||
7 | -- | ||
8 | -- Pieces are used to validate torrent content. | ||
9 | -- | ||
10 | {-# LANGUAGE TemplateHaskell #-} | ||
11 | {-# LANGUAGE DeriveDataTypeable #-} | ||
12 | {-# LANGUAGE DeriveFunctor #-} | ||
13 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} | ||
14 | module Data.Torrent.Piece | ||
15 | ( -- * Piece attributes | ||
16 | PieceIx | ||
17 | , PieceCount | ||
18 | , PieceSize | ||
19 | , minPieceSize | ||
20 | , maxPieceSize | ||
21 | , defaultPieceSize | ||
22 | , PieceHash | ||
23 | |||
24 | -- * Piece data | ||
25 | , Piece (..) | ||
26 | , pieceSize | ||
27 | , hashPiece | ||
28 | |||
29 | -- * Piece control | ||
30 | , HashList (..) | ||
31 | , PieceInfo (..) | ||
32 | , pieceCount | ||
33 | |||
34 | -- * Lens | ||
35 | , pieceLength | ||
36 | , pieceHashes | ||
37 | |||
38 | -- * Validation | ||
39 | , pieceHash | ||
40 | , checkPieceLazy | ||
41 | |||
42 | -- * Internal | ||
43 | , getPieceInfo | ||
44 | , putPieceInfo | ||
45 | ) where | ||
46 | |||
47 | import Control.DeepSeq | ||
48 | import Control.Lens | ||
49 | import qualified Crypto.Hash.SHA1 as SHA1 | ||
50 | import Data.BEncode | ||
51 | import Data.BEncode.Types | ||
52 | import Data.Bits | ||
53 | import Data.Bits.Extras | ||
54 | import Data.ByteString as BS | ||
55 | import qualified Data.ByteString.Lazy as BL | ||
56 | import qualified Data.ByteString.Base64 as Base64 | ||
57 | import Data.Default | ||
58 | import Data.Int | ||
59 | import Data.Text.Encoding as T | ||
60 | import Data.Typeable | ||
61 | import Text.PrettyPrint | ||
62 | import Text.PrettyPrint.Class | ||
63 | |||
64 | |||
65 | -- TODO add torrent file validation | ||
66 | class Lint a where | ||
67 | lint :: a -> Either String a | ||
68 | |||
69 | --class Validation a where | ||
70 | -- validate :: PieceInfo -> Piece a -> Bool | ||
71 | |||
72 | {----------------------------------------------------------------------- | ||
73 | -- Piece attributes | ||
74 | -----------------------------------------------------------------------} | ||
75 | |||
76 | -- | Zero-based index of piece in torrent content. | ||
77 | type PieceIx = Int | ||
78 | |||
79 | -- | Size of piece in bytes. Should be a power of 2. | ||
80 | -- | ||
81 | -- NOTE: Have max and min size constrained to wide used | ||
82 | -- semi-standard values. This bounds should be used to make decision | ||
83 | -- about piece size for new torrents. | ||
84 | -- | ||
85 | type PieceSize = Int | ||
86 | |||
87 | -- | Number of pieces in torrent or a part of torrent. | ||
88 | type PieceCount = Int | ||
89 | |||
90 | defaultBlockSize :: Int | ||
91 | defaultBlockSize = 16 * 1024 | ||
92 | |||
93 | -- | Optimal number of pieces in torrent. | ||
94 | optimalPieceCount :: PieceCount | ||
95 | optimalPieceCount = 1000 | ||
96 | {-# INLINE optimalPieceCount #-} | ||
97 | |||
98 | -- | Piece size should not be less than this value. | ||
99 | minPieceSize :: Int | ||
100 | minPieceSize = defaultBlockSize * 4 | ||
101 | {-# INLINE minPieceSize #-} | ||
102 | |||
103 | -- | To prevent transfer degradation piece size should not exceed this | ||
104 | -- value. | ||
105 | maxPieceSize :: Int | ||
106 | maxPieceSize = 4 * 1024 * 1024 | ||
107 | {-# INLINE maxPieceSize #-} | ||
108 | |||
109 | toPow2 :: Int -> Int | ||
110 | toPow2 x = bit $ fromIntegral (leadingZeros (0 :: Int) - leadingZeros x) | ||
111 | |||
112 | -- | Find the optimal piece size for a given torrent size. | ||
113 | defaultPieceSize :: Int64 -> Int | ||
114 | defaultPieceSize x = max minPieceSize $ min maxPieceSize $ toPow2 pc | ||
115 | where | ||
116 | pc = fromIntegral (x `div` fromIntegral optimalPieceCount) | ||
117 | |||
118 | {----------------------------------------------------------------------- | ||
119 | -- Piece data | ||
120 | -----------------------------------------------------------------------} | ||
121 | |||
122 | type PieceHash = ByteString | ||
123 | |||
124 | hashsize :: Int | ||
125 | hashsize = 20 | ||
126 | {-# INLINE hashsize #-} | ||
127 | |||
128 | -- TODO check if pieceLength is power of 2 | ||
129 | -- | Piece payload should be strict or lazy bytestring. | ||
130 | data Piece a = Piece | ||
131 | { -- | Zero-based piece index in torrent. | ||
132 | pieceIndex :: {-# UNPACK #-} !PieceIx | ||
133 | |||
134 | -- | Payload. | ||
135 | , pieceData :: !a | ||
136 | } deriving (Show, Read, Eq, Functor, Typeable) | ||
137 | |||
138 | instance NFData (Piece a) | ||
139 | |||
140 | -- | Payload bytes are omitted. | ||
141 | instance Pretty (Piece a) where | ||
142 | pretty Piece {..} = "Piece" <+> braces ("index" <+> "=" <+> int pieceIndex) | ||
143 | |||
144 | -- | Get size of piece in bytes. | ||
145 | pieceSize :: Piece BL.ByteString -> PieceSize | ||
146 | pieceSize Piece {..} = fromIntegral (BL.length pieceData) | ||
147 | |||
148 | -- | Get piece hash. | ||
149 | hashPiece :: Piece BL.ByteString -> PieceHash | ||
150 | hashPiece Piece {..} = SHA1.hashlazy pieceData | ||
151 | |||
152 | {----------------------------------------------------------------------- | ||
153 | -- Piece control | ||
154 | -----------------------------------------------------------------------} | ||
155 | |||
156 | -- | A flat array of SHA1 hash for each piece. | ||
157 | newtype HashList = HashList { unHashList :: ByteString } | ||
158 | deriving (Show, Read, Eq, BEncode, Typeable) | ||
159 | |||
160 | -- | Empty hash list. | ||
161 | instance Default HashList where | ||
162 | def = HashList "" | ||
163 | |||
164 | -- | Part of torrent file used for torrent content validation. | ||
165 | data PieceInfo = PieceInfo | ||
166 | { piPieceLength :: {-# UNPACK #-} !PieceSize | ||
167 | -- ^ Number of bytes in each piece. | ||
168 | |||
169 | , piPieceHashes :: !HashList | ||
170 | -- ^ Concatenation of all 20-byte SHA1 hash values. | ||
171 | } deriving (Show, Read, Eq, Typeable) | ||
172 | |||
173 | -- | Number of bytes in each piece. | ||
174 | makeLensesFor [("piPieceLength", "pieceLength")] ''PieceInfo | ||
175 | |||
176 | -- | Concatenation of all 20-byte SHA1 hash values. | ||
177 | makeLensesFor [("piPieceHashes", "pieceHashes")] ''PieceInfo | ||
178 | |||
179 | instance NFData PieceInfo | ||
180 | |||
181 | instance Default PieceInfo where | ||
182 | def = PieceInfo 1 def | ||
183 | |||
184 | instance Lint PieceInfo where | ||
185 | lint pinfo @ PieceInfo {..} | ||
186 | | BS.length (unHashList piPieceHashes) `rem` hashsize == 0 | ||
187 | , piPieceLength >= 0 = return pinfo | ||
188 | | otherwise = Left undefined | ||
189 | |||
190 | |||
191 | putPieceInfo :: PieceInfo -> BDict -> BDict | ||
192 | putPieceInfo PieceInfo {..} cont = | ||
193 | "piece length" .=! piPieceLength | ||
194 | .: "pieces" .=! piPieceHashes | ||
195 | .: cont | ||
196 | |||
197 | getPieceInfo :: Get PieceInfo | ||
198 | getPieceInfo = do | ||
199 | PieceInfo <$>! "piece length" | ||
200 | <*>! "pieces" | ||
201 | |||
202 | instance BEncode PieceInfo where | ||
203 | toBEncode = toDict . (`putPieceInfo` endDict) | ||
204 | fromBEncode = fromDict getPieceInfo | ||
205 | |||
206 | -- | Hashes are omitted. | ||
207 | instance Pretty PieceInfo where | ||
208 | pretty PieceInfo {..} = "Piece size: " <> int piPieceLength | ||
209 | |||
210 | slice :: Int -> Int -> ByteString -> ByteString | ||
211 | slice start len = BS.take len . BS.drop start | ||
212 | {-# INLINE slice #-} | ||
213 | |||
214 | -- | Extract validation hash by specified piece index. | ||
215 | pieceHash :: PieceInfo -> PieceIx -> PieceHash | ||
216 | pieceHash PieceInfo {..} i = slice (hashsize * i) hashsize (unHashList piPieceHashes) | ||
217 | |||
218 | -- | Find count of pieces in the torrent. If torrent size is not a | ||
219 | -- multiple of piece size then the count is rounded up. | ||
220 | pieceCount :: PieceInfo -> PieceCount | ||
221 | pieceCount PieceInfo {..} = BS.length (unHashList piPieceHashes) `quot` hashsize | ||
222 | |||
223 | -- | Test if this is last piece in torrent content. | ||
224 | isLastPiece :: PieceInfo -> PieceIx -> Bool | ||
225 | isLastPiece ci i = pieceCount ci == succ i | ||
226 | |||
227 | -- | Validate piece with metainfo hash. | ||
228 | checkPieceLazy :: PieceInfo -> Piece BL.ByteString -> Bool | ||
229 | checkPieceLazy pinfo @ PieceInfo {..} Piece {..} | ||
230 | = (fromIntegral (BL.length pieceData) == piPieceLength | ||
231 | || isLastPiece pinfo pieceIndex) | ||
232 | && SHA1.hashlazy pieceData == pieceHash pinfo pieceIndex | ||
diff --git a/src/Data/Torrent/Progress.hs b/src/Data/Torrent/Progress.hs deleted file mode 100644 index 4719020a..00000000 --- a/src/Data/Torrent/Progress.hs +++ /dev/null | |||
@@ -1,155 +0,0 @@ | |||
1 | -- | | ||
2 | -- Copyright : (c) Sam Truzjan 2013 | ||
3 | -- License : BSD3 | ||
4 | -- Maintainer : pxqr.sta@gmail.com | ||
5 | -- Stability : experimental | ||
6 | -- Portability : portable | ||
7 | -- | ||
8 | -- 'Progress' used to track amount downloaded\/left\/upload bytes | ||
9 | -- either on per client or per torrent basis. This value is used to | ||
10 | -- notify the tracker and usually shown to the user. To aggregate | ||
11 | -- total progress you can use the Monoid instance. | ||
12 | -- | ||
13 | {-# LANGUAGE TemplateHaskell #-} | ||
14 | {-# LANGUAGE ViewPatterns #-} | ||
15 | {-# OPTIONS -fno-warn-orphans #-} | ||
16 | module Data.Torrent.Progress | ||
17 | ( -- * Progress | ||
18 | Progress (..) | ||
19 | |||
20 | -- * Lens | ||
21 | , left | ||
22 | , uploaded | ||
23 | , downloaded | ||
24 | |||
25 | -- * Construction | ||
26 | , startProgress | ||
27 | , downloadedProgress | ||
28 | , enqueuedProgress | ||
29 | , uploadedProgress | ||
30 | , dequeuedProgress | ||
31 | |||
32 | -- * Query | ||
33 | , canDownload | ||
34 | , canUpload | ||
35 | ) where | ||
36 | |||
37 | import Control.Applicative | ||
38 | import Control.Lens hiding ((%=)) | ||
39 | import Data.ByteString.Lazy.Builder as BS | ||
40 | import Data.ByteString.Lazy.Builder.ASCII as BS | ||
41 | import Data.Default | ||
42 | import Data.List as L | ||
43 | import Data.Monoid | ||
44 | import Data.Serialize as S | ||
45 | import Data.Ratio | ||
46 | import Data.Word | ||
47 | import Network.HTTP.Types.QueryLike | ||
48 | import Text.PrettyPrint as PP | ||
49 | import Text.PrettyPrint.Class | ||
50 | |||
51 | |||
52 | -- | Progress data is considered as dynamic within one client | ||
53 | -- session. This data also should be shared across client application | ||
54 | -- sessions (e.g. files), otherwise use 'startProgress' to get initial | ||
55 | -- 'Progress' value. | ||
56 | -- | ||
57 | data Progress = Progress | ||
58 | { _downloaded :: {-# UNPACK #-} !Word64 -- ^ Total amount of bytes downloaded; | ||
59 | , _left :: {-# UNPACK #-} !Word64 -- ^ Total amount of bytes left; | ||
60 | , _uploaded :: {-# UNPACK #-} !Word64 -- ^ Total amount of bytes uploaded. | ||
61 | } deriving (Show, Read, Eq) | ||
62 | |||
63 | $(makeLenses ''Progress) | ||
64 | |||
65 | -- | UDP tracker compatible encoding. | ||
66 | instance Serialize Progress where | ||
67 | put Progress {..} = do | ||
68 | putWord64be $ fromIntegral _downloaded | ||
69 | putWord64be $ fromIntegral _left | ||
70 | putWord64be $ fromIntegral _uploaded | ||
71 | |||
72 | get = Progress | ||
73 | <$> (fromIntegral <$> getWord64be) | ||
74 | <*> (fromIntegral <$> getWord64be) | ||
75 | <*> (fromIntegral <$> getWord64be) | ||
76 | |||
77 | instance Default Progress where | ||
78 | def = Progress 0 0 0 | ||
79 | {-# INLINE def #-} | ||
80 | |||
81 | -- | Can be used to aggregate total progress. | ||
82 | instance Monoid Progress where | ||
83 | mempty = def | ||
84 | {-# INLINE mempty #-} | ||
85 | |||
86 | mappend (Progress da la ua) (Progress db lb ub) = Progress | ||
87 | { _downloaded = da + db | ||
88 | , _left = la + lb | ||
89 | , _uploaded = ua + ub | ||
90 | } | ||
91 | {-# INLINE mappend #-} | ||
92 | |||
93 | instance QueryValueLike Builder where | ||
94 | toQueryValue = toQueryValue . BS.toLazyByteString | ||
95 | |||
96 | instance QueryValueLike Word64 where | ||
97 | toQueryValue = toQueryValue . BS.word64Dec | ||
98 | |||
99 | -- | HTTP Tracker protocol compatible encoding. | ||
100 | instance QueryLike Progress where | ||
101 | toQuery Progress {..} = | ||
102 | [ ("uploaded" , toQueryValue _uploaded) | ||
103 | , ("left" , toQueryValue _left) | ||
104 | , ("downloaded", toQueryValue _downloaded) | ||
105 | ] | ||
106 | |||
107 | instance Pretty Progress where | ||
108 | pretty Progress {..} = | ||
109 | "/\\" <+> PP.text (show _uploaded) $$ | ||
110 | "\\/" <+> PP.text (show _downloaded) $$ | ||
111 | "left" <+> PP.text (show _left) | ||
112 | |||
113 | -- | Initial progress is used when there are no session before. | ||
114 | -- | ||
115 | -- Please note that tracker might penalize client some way if the do | ||
116 | -- not accumulate progress. If possible and save 'Progress' between | ||
117 | -- client sessions to avoid that. | ||
118 | -- | ||
119 | startProgress :: Integer -> Progress | ||
120 | startProgress = Progress 0 0 . fromIntegral | ||
121 | {-# INLINE startProgress #-} | ||
122 | |||
123 | -- | Used when the client download some data from /any/ peer. | ||
124 | downloadedProgress :: Int -> Progress -> Progress | ||
125 | downloadedProgress (fromIntegral -> amount) | ||
126 | = (left -~ amount) | ||
127 | . (downloaded +~ amount) | ||
128 | {-# INLINE downloadedProgress #-} | ||
129 | |||
130 | -- | Used when the client upload some data to /any/ peer. | ||
131 | uploadedProgress :: Int -> Progress -> Progress | ||
132 | uploadedProgress (fromIntegral -> amount) = uploaded +~ amount | ||
133 | {-# INLINE uploadedProgress #-} | ||
134 | |||
135 | -- | Used when leecher join client session. | ||
136 | enqueuedProgress :: Integer -> Progress -> Progress | ||
137 | enqueuedProgress amount = left +~ fromIntegral amount | ||
138 | {-# INLINE enqueuedProgress #-} | ||
139 | |||
140 | -- | Used when leecher leave client session. | ||
141 | -- (e.g. user deletes not completed torrent) | ||
142 | dequeuedProgress :: Integer -> Progress -> Progress | ||
143 | dequeuedProgress amount = left -~ fromIntegral amount | ||
144 | {-# INLINE dequeuedProgress #-} | ||
145 | |||
146 | ri2rw64 :: Ratio Int -> Ratio Word64 | ||
147 | ri2rw64 x = fromIntegral (numerator x) % fromIntegral (denominator x) | ||
148 | |||
149 | -- | Check global /download/ limit by uploaded \/ downloaded ratio. | ||
150 | canDownload :: Ratio Int -> Progress -> Bool | ||
151 | canDownload limit Progress {..} = _uploaded % _downloaded > ri2rw64 limit | ||
152 | |||
153 | -- | Check global /upload/ limit by downloaded \/ uploaded ratio. | ||
154 | canUpload :: Ratio Int -> Progress -> Bool | ||
155 | canUpload limit Progress {..} = _downloaded % _uploaded > ri2rw64 limit | ||
diff --git a/src/Data/Torrent/Tree.hs b/src/Data/Torrent/Tree.hs deleted file mode 100644 index 102f4dff..00000000 --- a/src/Data/Torrent/Tree.hs +++ /dev/null | |||
@@ -1,83 +0,0 @@ | |||
1 | -- | | ||
2 | -- Copyright : (c) Sam Truzjan 2013 | ||
3 | -- License : BSD3 | ||
4 | -- Maintainer : pxqr.sta@gmail.com | ||
5 | -- Stability : experimental | ||
6 | -- Portability : portable | ||
7 | -- | ||
8 | -- Directory tree can be used to easily manipulate file layout info. | ||
9 | -- | ||
10 | {-# LANGUAGE FlexibleInstances #-} | ||
11 | {-# LANGUAGE TemplateHaskell #-} | ||
12 | {-# LANGUAGE DeriveDataTypeable #-} | ||
13 | module Data.Torrent.Tree | ||
14 | ( -- * Directory tree | ||
15 | DirTree (..) | ||
16 | |||
17 | -- * Construction | ||
18 | , build | ||
19 | |||
20 | -- * Query | ||
21 | , Data.Torrent.Tree.lookup | ||
22 | , lookupDir | ||
23 | , fileNumber | ||
24 | , dirNumber | ||
25 | ) where | ||
26 | |||
27 | import Data.ByteString as BS | ||
28 | import Data.ByteString.Char8 as BC | ||
29 | import Data.Foldable | ||
30 | import Data.List as L | ||
31 | import Data.Map as M | ||
32 | import Data.Monoid | ||
33 | |||
34 | import Data.Torrent.Layout | ||
35 | |||
36 | |||
37 | -- | 'DirTree' is more convenient form of 'LayoutInfo'. | ||
38 | data DirTree a = Dir { children :: Map ByteString (DirTree a) } | ||
39 | | File { node :: FileInfo a } | ||
40 | deriving Show | ||
41 | |||
42 | -- | Build directory tree from a list of files. | ||
43 | build :: LayoutInfo -> DirTree () | ||
44 | build SingleFile {liFile = FileInfo {..}} = Dir | ||
45 | { children = M.singleton fiName (File fi) } | ||
46 | where | ||
47 | fi = FileInfo fiLength fiMD5Sum () | ||
48 | build MultiFile {..} = Dir $ M.singleton liDirName files | ||
49 | where | ||
50 | files = Dir $ M.fromList $ L.map mkFileEntry liFiles | ||
51 | mkFileEntry FileInfo {..} = (L.head fiName, ent) -- TODO FIXME | ||
52 | where | ||
53 | ent = File $ FileInfo fiLength fiMD5Sum () | ||
54 | |||
55 | --decompress :: DirTree () -> [FileInfo ()] | ||
56 | --decompress = undefined | ||
57 | |||
58 | -- TODO pretty print | ||
59 | |||
60 | -- | Lookup file by path. | ||
61 | lookup :: [FilePath] -> DirTree a -> Maybe (DirTree a) | ||
62 | lookup [] t = Just t | ||
63 | lookup (p : ps) (Dir m) | Just subTree <- M.lookup (BC.pack p) m | ||
64 | = Data.Torrent.Tree.lookup ps subTree | ||
65 | lookup _ _ = Nothing | ||
66 | |||
67 | -- | Lookup directory by path. | ||
68 | lookupDir :: [FilePath] -> DirTree a -> Maybe [(ByteString, DirTree a)] | ||
69 | lookupDir ps d = do | ||
70 | subTree <- Data.Torrent.Tree.lookup ps d | ||
71 | case subTree of | ||
72 | File _ -> Nothing | ||
73 | Dir es -> Just $ M.toList es | ||
74 | |||
75 | -- | Get total count of files in directory and subdirectories. | ||
76 | fileNumber :: DirTree a -> Sum Int | ||
77 | fileNumber File {..} = Sum 1 | ||
78 | fileNumber Dir {..} = foldMap fileNumber children | ||
79 | |||
80 | -- | Get total count of directories in the directory and subdirectories. | ||
81 | dirNumber :: DirTree a -> Sum Int | ||
82 | dirNumber File {..} = Sum 0 | ||
83 | dirNumber Dir {..} = Sum 1 <> foldMap dirNumber children | ||