summaryrefslogtreecommitdiff
path: root/src/Data
diff options
context:
space:
mode:
Diffstat (limited to 'src/Data')
-rw-r--r--src/Data/Torrent.hs989
-rw-r--r--src/Data/Torrent/Bitfield.hs324
-rw-r--r--src/Data/Torrent/InfoHash.hs164
-rw-r--r--src/Data/Torrent/Layout.hs321
-rw-r--r--src/Data/Torrent/Magnet.hs372
-rw-r--r--src/Data/Torrent/Piece.hs232
-rw-r--r--src/Data/Torrent/Progress.hs155
-rw-r--r--src/Data/Torrent/Tree.hs83
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 #-}
27module Data.Torrent 33module 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
68import Prelude hiding (sum) 147import Prelude
69import Control.Applicative 148import Control.Applicative
70import qualified Crypto.Hash.SHA1 as C
71import Control.DeepSeq 149import Control.DeepSeq
72import Control.Exception 150import Control.Exception
73import Control.Lens 151import Control.Lens
152import Control.Monad
153import Crypto.Hash.SHA1 as SHA1
74import Data.BEncode as BE 154import Data.BEncode as BE
75import Data.BEncode.Types as BE 155import Data.BEncode.Types as BE
76import Data.ByteString as BS 156import Data.Bits
77import qualified Data.ByteString.Char8 as BC (pack, unpack) 157import Data.Bits.Extras
78import qualified Data.ByteString.Lazy as BL 158import Data.ByteString as BS
79import Data.Convertible 159import Data.ByteString.Base16 as Base16
80import Data.Default 160import Data.ByteString.Base32 as Base32
81import Data.Hashable as Hashable 161import Data.ByteString.Base64 as Base64
82import qualified Data.List as L 162import Data.ByteString.Char8 as BC (pack, unpack)
83import Data.Text as T 163import Data.ByteString.Lazy as BL
84import Data.Time 164import Data.Char
165import Data.Convertible
166import Data.Default
167import Data.Foldable as F
168import Data.Hashable as Hashable
169import Data.Int
170import Data.List as L
171import Data.Map as M
172import Data.Maybe
173import Data.Serialize as S
174import Data.String
175import Data.Text as T
176import Data.Text.Encoding as T
177import Data.Text.Read
85import Data.Time.Clock.POSIX 178import Data.Time.Clock.POSIX
86import Data.Typeable 179import Data.Typeable
87import Network (HostName) 180import Network (HostName)
181import Network.HTTP.Types.QueryLike
182import Network.HTTP.Types.URI
88import Network.URI 183import Network.URI
184import Text.ParserCombinators.ReadP as P
89import Text.PrettyPrint as PP 185import Text.PrettyPrint as PP
90import Text.PrettyPrint.Class 186import Text.PrettyPrint.Class
91import System.FilePath 187import System.FilePath
188import System.Posix.Types
189
190import 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.
211newtype InfoHash = InfoHash { getInfoHash :: BS.ByteString }
212 deriving (Eq, Ord, Typeable)
213
214infoHashLen :: Int
215infoHashLen = 20
216
217-- | Meaningless placeholder value.
218instance Default InfoHash where
219 def = "0123456789012345678901234567890123456789"
220
221-- | Hash raw bytes. (no encoding)
222instance Hashable InfoHash where
223 hashWithSalt s (InfoHash ih) = hashWithSalt s ih
224 {-# INLINE hashWithSalt #-}
225
226-- | Convert to\/from raw bencoded string. (no encoding)
227instance BEncode InfoHash where
228 toBEncode = toBEncode . getInfoHash
229 fromBEncode be = InfoHash <$> fromBEncode be
230
231-- | Convert to\/from raw bytestring. (no encoding)
232instance 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)
240instance QueryValueLike InfoHash where
241 toQueryValue (InfoHash ih) = Just ih
242 {-# INLINE toQueryValue #-}
243
244-- | Convert to base16 encoded string.
245instance Show InfoHash where
246 show (InfoHash ih) = BC.unpack (Base16.encode ih)
247
248-- | Convert to base16 encoded Doc string.
249instance Pretty InfoHash where
250 pretty = text . show
251
252-- | Read base16 encoded string.
253instance 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.
265instance 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.
271instance 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.
296instance IsString InfoHash where
297 fromString = either (error . prettyConvertError) id . safeConvert . T.pack
298
299ignoreErrorMsg :: Either a b -> Maybe b
300ignoreErrorMsg = either (const Nothing) Just
301
302-- | Tries both base16 and base32 while decoding info hash.
303--
304-- Use 'safeConvert' for detailed error messages.
305--
306textToInfoHash :: Text -> Maybe InfoHash
307textToInfoHash = ignoreErrorMsg . safeConvert
92 308
93import Data.Torrent.InfoHash as IH 309-- | Hex encode infohash to text, full length.
94import Data.Torrent.Layout 310longHex :: InfoHash -> Text
95import Data.Torrent.Piece 311longHex = T.decodeUtf8 . Base16.encode . getInfoHash
96import Network.BitTorrent.Core.NodeInfo 312
313-- | The same as 'longHex', but only first 7 characters.
314shortHex :: InfoHash -> Text
315shortHex = T.take 7 . longHex
316
317{-----------------------------------------------------------------------
318-- File info
319-----------------------------------------------------------------------}
320
321-- | Size of a file in bytes.
322type FileSize = FileOffset
323
324deriving instance BEncode FileOffset
325
326-- | Contain metainfo about one single file.
327data 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
352makeLensesFor
353 [ ("fiLength", "fileLength")
354 , ("fiMD5Sum", "fileMD5Sum")
355 , ("fiName" , "filePath" )
356 ]
357 ''FileInfo
358
359instance NFData a => NFData (FileInfo a) where
360 rnf FileInfo {..} = rnf fiName
361 {-# INLINE rnf #-}
362
363instance 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
377type Put a = a -> BDict -> BDict
378
379putFileInfoSingle :: Data.Torrent.Put (FileInfo BS.ByteString)
380putFileInfoSingle FileInfo {..} cont =
381 "length" .=! fiLength
382 .: "md5sum" .=? fiMD5Sum
383 .: "name" .=! fiName
384 .: cont
385
386getFileInfoSingle :: BE.Get (FileInfo BS.ByteString)
387getFileInfoSingle = do
388 FileInfo <$>! "length"
389 <*>? "md5sum"
390 <*>! "name"
391
392instance BEncode (FileInfo BS.ByteString) where
393 toBEncode = toDict . (`putFileInfoSingle` endDict)
394 {-# INLINE toBEncode #-}
395
396 fromBEncode = fromDict getFileInfoSingle
397 {-# INLINE fromBEncode #-}
398
399instance 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.
408joinFilePath :: FileInfo [BS.ByteString] -> FileInfo BS.ByteString
409joinFilePath = 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--
421data 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
435makeLensesFor
436 [ ("liFile" , "singleFile" )
437 , ("liFiles" , "multiFile" )
438 , ("liDirName", "rootDirName")
439 ]
440 ''LayoutInfo
441
442instance NFData LayoutInfo where
443 rnf SingleFile {..} = ()
444 rnf MultiFile {..} = rnf liFiles
445
446-- | Empty multifile layout.
447instance Default LayoutInfo where
448 def = MultiFile [] ""
449
450getLayoutInfo :: BE.Get LayoutInfo
451getLayoutInfo = single <|> multi
452 where
453 single = SingleFile <$> getFileInfoSingle
454 multi = MultiFile <$>! "files" <*>! "name"
455
456putLayoutInfo :: Data.Torrent.Put LayoutInfo
457putLayoutInfo SingleFile {..} = putFileInfoSingle liFile
458putLayoutInfo MultiFile {..} = \ cont ->
459 "files" .=! liFiles
460 .: "name" .=! liDirName
461 .: cont
462
463instance BEncode LayoutInfo where
464 toBEncode = toDict . (`putLayoutInfo` endDict)
465 fromBEncode = fromDict getLayoutInfo
466
467instance 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.
472isSingleFile :: LayoutInfo -> Bool
473isSingleFile SingleFile {} = True
474isSingleFile _ = False
475{-# INLINE isSingleFile #-}
476
477-- | Test if this is multifile torrent.
478isMultiFile :: LayoutInfo -> Bool
479isMultiFile MultiFile {} = True
480isMultiFile _ = False
481{-# INLINE isMultiFile #-}
482
483-- | Get name of the torrent based on the root path piece.
484suggestedName :: LayoutInfo -> BS.ByteString
485suggestedName (SingleFile FileInfo {..}) = fiName
486suggestedName MultiFile {..} = liDirName
487{-# INLINE suggestedName #-}
488
489-- | Find sum of sizes of the all torrent files.
490contentLength :: LayoutInfo -> FileSize
491contentLength SingleFile { liFile = FileInfo {..} } = fiLength
492contentLength MultiFile { liFiles = tfs } = L.sum (L.map fiLength tfs)
493
494-- | Get number of all files in torrent.
495fileCount :: LayoutInfo -> Int
496fileCount SingleFile {..} = 1
497fileCount 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.
501blockCount :: Int -> LayoutInfo -> Int
502blockCount 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--
511type FileLayout a = [(FilePath, a)]
512
513-- | Extract files layout from torrent info with the given root path.
514flatLayout
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.
518flatLayout prefixPath SingleFile { liFile = FileInfo {..} }
519 = [(prefixPath </> BC.unpack fiName, fiLength)]
520flatLayout 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.
528accumPositions :: FileLayout FileSize -> FileLayout (FileOffset, FileSize)
529accumPositions = 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.
535fileOffset :: FilePath -> FileLayout FileOffset -> Maybe FileOffset
536fileOffset = L.lookup
537{-# INLINE fileOffset #-}
538
539------------------------------------------------------------------------
540
541-- | Divide and round up.
542sizeInBase :: Integral a => a -> Int -> Int
543sizeInBase 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.
554type 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--
562type PieceSize = Int
563
564-- | Number of pieces in torrent or a part of torrent.
565type PieceCount = Int
566
567defaultBlockSize :: Int
568defaultBlockSize = 16 * 1024
569
570-- | Optimal number of pieces in torrent.
571optimalPieceCount :: PieceCount
572optimalPieceCount = 1000
573{-# INLINE optimalPieceCount #-}
574
575-- | Piece size should not be less than this value.
576minPieceSize :: Int
577minPieceSize = defaultBlockSize * 4
578{-# INLINE minPieceSize #-}
579
580-- | To prevent transfer degradation piece size should not exceed this
581-- value.
582maxPieceSize :: Int
583maxPieceSize = 4 * 1024 * 1024
584{-# INLINE maxPieceSize #-}
585
586toPow2 :: Int -> Int
587toPow2 x = bit $ fromIntegral (leadingZeros (0 :: Int) - leadingZeros x)
588
589-- | Find the optimal piece size for a given torrent size.
590defaultPieceSize :: Int64 -> Int
591defaultPieceSize x = max minPieceSize $ min maxPieceSize $ toPow2 pc
592 where
593 pc = fromIntegral (x `div` fromIntegral optimalPieceCount)
594
595{-----------------------------------------------------------------------
596-- Piece data
597-----------------------------------------------------------------------}
598
599type PieceHash = BS.ByteString
600
601hashsize :: Int
602hashsize = 20
603{-# INLINE hashsize #-}
604
605-- TODO check if pieceLength is power of 2
606-- | Piece payload should be strict or lazy bytestring.
607data 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
615instance NFData (Piece a)
616
617-- | Payload bytes are omitted.
618instance Pretty (Piece a) where
619 pretty Piece {..} = "Piece" <+> braces ("index" <+> "=" <+> int pieceIndex)
620
621-- | Get size of piece in bytes.
622pieceSize :: Piece BL.ByteString -> PieceSize
623pieceSize Piece {..} = fromIntegral (BL.length pieceData)
624
625-- | Get piece hash.
626hashPiece :: Piece BL.ByteString -> PieceHash
627hashPiece Piece {..} = SHA1.hashlazy pieceData
628
629{-----------------------------------------------------------------------
630-- Piece control
631-----------------------------------------------------------------------}
632
633-- | A flat array of SHA1 hash for each piece.
634newtype HashList = HashList { unHashList :: BS.ByteString }
635 deriving (Show, Read, Eq, BEncode, Typeable)
636
637-- | Empty hash list.
638instance Default HashList where
639 def = HashList ""
640
641-- | Part of torrent file used for torrent content validation.
642data 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.
651makeLensesFor [("piPieceLength", "pieceLength")] ''PieceInfo
652
653-- | Concatenation of all 20-byte SHA1 hash values.
654makeLensesFor [("piPieceHashes", "pieceHashes")] ''PieceInfo
655
656instance NFData PieceInfo
657
658instance Default PieceInfo where
659 def = PieceInfo 1 def
660
661class Lint a where
662 lint :: a -> Either String a
663
664instance 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
671putPieceInfo :: Data.Torrent.Put PieceInfo
672putPieceInfo PieceInfo {..} cont =
673 "piece length" .=! piPieceLength
674 .: "pieces" .=! piPieceHashes
675 .: cont
676
677getPieceInfo :: BE.Get PieceInfo
678getPieceInfo = do
679 PieceInfo <$>! "piece length"
680 <*>! "pieces"
681
682instance BEncode PieceInfo where
683 toBEncode = toDict . (`putPieceInfo` endDict)
684 fromBEncode = fromDict getPieceInfo
685
686-- | Hashes are omitted.
687instance Pretty PieceInfo where
688 pretty PieceInfo {..} = "Piece size: " <> int piPieceLength
689
690slice :: Int -> Int -> BS.ByteString -> BS.ByteString
691slice start len = BS.take len . BS.drop start
692{-# INLINE slice #-}
693
694-- | Extract validation hash by specified piece index.
695pieceHash :: PieceInfo -> PieceIx -> PieceHash
696pieceHash 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.
700pieceCount :: PieceInfo -> PieceCount
701pieceCount PieceInfo {..} = BS.length (unHashList piPieceHashes) `quot` hashsize
702
703-- | Test if this is last piece in torrent content.
704isLastPiece :: PieceInfo -> PieceIx -> Bool
705isLastPiece ci i = pieceCount ci == succ i
706
707-- | Validate piece with metainfo hash.
708checkPieceLazy :: PieceInfo -> Piece BL.ByteString -> Bool
709checkPieceLazy 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
145infoDictionary :: LayoutInfo -> PieceInfo -> Bool -> InfoDict 761infoDictionary :: LayoutInfo -> PieceInfo -> Bool -> InfoDict
146infoDictionary li pinfo private = InfoDict ih li pinfo private 762infoDictionary 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
150getPrivate :: Get Bool 766getPrivate :: BE.Get Bool
151getPrivate = (Just True ==) <$>? "private" 767getPrivate = (Just True ==) <$>? "private"
152 768
153putPrivate :: Bool -> BDict -> BDict 769putPrivate :: 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.
158hashLazyIH :: BL.ByteString -> InfoHash 774hashLazyIH :: BL.ByteString -> InfoHash
159hashLazyIH = either (const (error msg)) id . safeConvert . C.hashlazy 775hashLazyIH = 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
177ppPrivacy :: Bool -> Doc 793ppPrivacy :: Bool -> Doc
178ppPrivacy privacy = "Privacy: " <> if privacy then "private" else "public" 794ppPrivacy 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.
194data Torrent = Torrent 811data 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
361fromFile :: FilePath -> IO Torrent 978fromFile :: FilePath -> IO Torrent
362fromFile filepath = do 979fromFile 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.
369toFile :: FilePath -> Torrent -> IO () 986toFile :: FilePath -> Torrent -> IO ()
370toFile filepath = BL.writeFile filepath . encode 987toFile filepath = BL.writeFile filepath . BE.encode
988
989{-----------------------------------------------------------------------
990-- URN
991-----------------------------------------------------------------------}
992
993-- | Namespace identifier determines the syntactic interpretation of
994-- namespace-specific string.
995type 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--
1001btih :: NamespaceId
1002btih = ["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--
1009data 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
1017instance 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--
1025infohashURN :: InfoHash -> URN
1026infohashURN = URN btih . longHex
1027
1028-- | Meaningless placeholder value.
1029instance Default URN where
1030 def = infohashURN def
1031
1032------------------------------------------------------------------------
1033
1034-- | Render URN to its text representation.
1035renderURN :: URN -> Text
1036renderURN URN {..}
1037 = T.intercalate ":" $ "urn" : urnNamespace ++ [urnString]
1038
1039instance Pretty URN where
1040 pretty = text . T.unpack . renderURN
1041
1042instance Show URN where
1043 showsPrec n = showsPrec n . T.unpack . renderURN
1044
1045instance 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
1055instance 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
1068instance 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--
1076parseURN :: Text -> Maybe URN
1077parseURN = 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.
1104data 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
1137instance QueryValueLike Integer where
1138 toQueryValue = toQueryValue . show
1139
1140instance QueryValueLike URI where
1141 toQueryValue = toQueryValue . show
1142
1143instance 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
1155instance QueryValueLike Magnet where
1156 toQueryValue = toQueryValue . renderMagnet
1157
1158instance 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
1179magnetScheme :: URI
1180magnetScheme = URI
1181 { uriScheme = "magnet:"
1182 , uriAuthority = Nothing
1183 , uriPath = ""
1184 , uriQuery = ""
1185 , uriFragment = ""
1186 }
1187
1188isMagnetURI :: URI -> Bool
1189isMagnetURI u = u { uriQuery = "" } == magnetScheme
1190
1191-- | Can be used instead of 'parseMagnet'.
1192instance 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'.
1198instance Convertible Magnet URI where
1199 safeConvert m = pure $ magnetScheme
1200 { uriQuery = BC.unpack $ renderQuery True $ toQuery m }
1201
1202instance 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.
1210instance 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.
1224nullMagnet :: InfoHash -> Magnet
1225nullMagnet 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).
1238simpleMagnet :: Torrent -> Magnet
1239simpleMagnet 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--
1247detailedMagnet :: Torrent -> Magnet
1248detailedMagnet t @ Torrent {tInfoDict = InfoDict {..}, tAnnounce}
1249 = (simpleMagnet t)
1250 { exactLength = Just $ fromIntegral $ contentLength idLayoutInfo
1251 , tracker = tAnnounce
1252 }
1253
1254-----------------------------------------------------------------------
1255
1256parseMagnetStr :: String -> Maybe Magnet
1257parseMagnetStr = either (const Nothing) Just . safeConvert
1258
1259renderMagnetStr :: Magnet -> String
1260renderMagnetStr = show . (convert :: Magnet -> URI)
1261
1262instance Pretty Magnet where
1263 pretty = PP.text . renderMagnetStr
1264
1265instance Show Magnet where
1266 show = renderMagnetStr
1267 {-# INLINE show #-}
1268
1269instance Read Magnet where
1270 readsPrec _ xs
1271 | Just m <- parseMagnetStr mstr = [(m, rest)]
1272 | otherwise = []
1273 where
1274 (mstr, rest) = L.break (== ' ') xs
1275
1276instance 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--
1284parseMagnet :: Text -> Maybe Magnet
1285parseMagnet = parseMagnetStr . T.unpack
1286{-# INLINE parseMagnet #-}
1287
1288-- | Render magnet link to urlencoded string
1289renderMagnet :: Magnet -> Text
1290renderMagnet = 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 #-}
30module 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
80import Control.Monad
81import Control.Monad.ST
82import Data.ByteString (ByteString)
83import qualified Data.ByteString as B
84import qualified Data.ByteString.Lazy as Lazy
85import Data.Vector.Unboxed (Vector)
86import qualified Data.Vector.Unboxed as V
87import qualified Data.Vector.Unboxed.Mutable as VM
88import Data.IntervalSet (IntSet)
89import qualified Data.IntervalSet as S
90import qualified Data.IntervalSet.ByteString as S
91import Data.List (foldl')
92import Data.Monoid
93import Data.Ratio
94
95import 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--
105data 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
112instance 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.
123haveNone :: PieceCount -> Bitfield
124haveNone s = Bitfield s S.empty
125
126-- | The full bitfield containing all piece indices for the given size.
127haveAll :: PieceCount -> Bitfield
128haveAll s = Bitfield s (S.interval 0 (s - 1))
129
130-- | Insert the index in the set ignoring out of range indices.
131have :: PieceIx -> Bitfield -> Bitfield
132have ix Bitfield {..}
133 | 0 <= ix && ix < bfSize = Bitfield bfSize (S.insert ix bfSet)
134 | otherwise = Bitfield bfSize bfSet
135
136singleton :: PieceIx -> PieceCount -> Bitfield
137singleton ix pc = have ix (haveNone pc)
138
139-- | Assign new size to bitfield. FIXME Normally, size should be only
140-- decreased, otherwise exception raised.
141adjustSize :: PieceCount -> Bitfield -> Bitfield
142adjustSize s Bitfield {..} = Bitfield s bfSet
143
144-- | NOTE: for internal use only
145interval :: PieceCount -> PieceIx -> PieceIx -> Bitfield
146interval 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.
153null :: Bitfield -> Bool
154null Bitfield {..} = S.null bfSet
155
156-- | Test if bitfield have all pieces.
157full :: Bitfield -> Bool
158full Bitfield {..} = S.size bfSet == bfSize
159
160-- | Count of peer have pieces.
161haveCount :: Bitfield -> PieceCount
162haveCount = S.size . bfSet
163
164-- | Total count of pieces and its indices.
165totalCount :: Bitfield -> PieceCount
166totalCount = bfSize
167
168-- | Ratio of /have/ piece count to the /total/ piece count.
169--
170-- > forall bf. 0 <= completeness bf <= 1
171--
172completeness :: Bitfield -> Ratio PieceCount
173completeness b = haveCount b % totalCount b
174
175inRange :: PieceIx -> Bitfield -> Bool
176inRange ix Bitfield {..} = 0 <= ix && ix < bfSize
177
178member :: PieceIx -> Bitfield -> Bool
179member ix bf @ Bitfield {..}
180 | ix `inRange` bf = ix `S.member` bfSet
181 | otherwise = False
182
183notMember :: PieceIx -> Bitfield -> Bool
184notMember ix bf @ Bitfield {..}
185 | ix `inRange` bf = ix `S.notMember` bfSet
186 | otherwise = True
187
188-- | Find first available piece index.
189findMin :: Bitfield -> PieceIx
190findMin = S.findMin . bfSet
191{-# INLINE findMin #-}
192
193-- | Find last available piece index.
194findMax :: Bitfield -> PieceIx
195findMax = S.findMax . bfSet
196{-# INLINE findMax #-}
197
198-- | Check if all pieces from first bitfield present if the second bitfield
199isSubsetOf :: Bitfield -> Bitfield -> Bool
200isSubsetOf a b = bfSet a `S.isSubsetOf` bfSet b
201{-# INLINE isSubsetOf #-}
202
203-- | Resulting bitfield includes only missing pieces.
204complement :: Bitfield -> Bitfield
205complement 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.
220type Frequency = Int
221
222-- TODO rename to availability
223-- | How many times each piece index occur in the given bitfield set.
224frequencies :: [Bitfield] -> Vector Frequency
225frequencies [] = V.fromList []
226frequencies 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'.
241rarest :: [Bitfield] -> Maybe PieceIx
242rarest 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
261insert :: PieceIx -> Bitfield -> Bitfield
262insert 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.
270union :: Bitfield -> Bitfield -> Bitfield
271union 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.
277intersection :: Bitfield -> Bitfield -> Bitfield
278intersection 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.
284difference :: Bitfield -> Bitfield -> Bitfield
285difference 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.
291unions :: [Bitfield] -> Bitfield
292unions = {-# SCC unions #-} foldl' union (haveNone 0)
293
294{-----------------------------------------------------------------------
295 Serialization
296-----------------------------------------------------------------------}
297
298-- | List all /have/ indexes.
299toList :: Bitfield -> [PieceIx]
300toList Bitfield {..} = S.toList bfSet
301
302-- | Make bitfield from list of /have/ indexes.
303fromList :: PieceCount -> [PieceIx] -> Bitfield
304fromList 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'.
311fromBitmap :: ByteString -> Bitfield
312fromBitmap 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.
319toBitmap :: Bitfield -> Lazy.ByteString
320toBitmap 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 #-}
14module Data.Torrent.InfoHash
15 ( InfoHash
16
17 -- * Parsing
18 , textToInfoHash
19
20 -- * Rendering
21 , longHex
22 , shortHex
23 ) where
24
25import Control.Applicative
26import Control.Monad
27import Data.BEncode
28import Data.ByteString as BS
29import Data.ByteString.Char8 as BC
30import Data.ByteString.Base16 as Base16
31import Data.ByteString.Base32 as Base32
32import Data.ByteString.Base64 as Base64
33import Data.Char
34import Data.Convertible.Base
35import Data.Default
36import Data.List as L
37import Data.Hashable as Hashable
38import Data.Serialize
39import Data.String
40import Data.Text as T
41import Data.Text.Encoding as T
42import Data.Typeable
43import Network.HTTP.Types.QueryLike
44import Text.ParserCombinators.ReadP as P
45import Text.PrettyPrint
46import 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.
60newtype InfoHash = InfoHash { getInfoHash :: BS.ByteString }
61 deriving (Eq, Ord, Typeable)
62
63infoHashLen :: Int
64infoHashLen = 20
65
66-- | Meaningless placeholder value.
67instance Default InfoHash where
68 def = "0123456789012345678901234567890123456789"
69
70-- | Hash raw bytes. (no encoding)
71instance Hashable InfoHash where
72 hashWithSalt s (InfoHash ih) = hashWithSalt s ih
73 {-# INLINE hashWithSalt #-}
74
75-- | Convert to\/from raw bencoded string. (no encoding)
76instance BEncode InfoHash where
77 toBEncode = toBEncode . getInfoHash
78 fromBEncode be = InfoHash <$> fromBEncode be
79
80-- | Convert to\/from raw bytestring. (no encoding)
81instance 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)
89instance QueryValueLike InfoHash where
90 toQueryValue (InfoHash ih) = Just ih
91 {-# INLINE toQueryValue #-}
92
93-- | Convert to base16 encoded string.
94instance Show InfoHash where
95 show (InfoHash ih) = BC.unpack (Base16.encode ih)
96
97-- | Convert to base16 encoded Doc string.
98instance Pretty InfoHash where
99 pretty = text . show
100
101-- | Read base16 encoded string.
102instance 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.
114instance 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.
120instance 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.
145instance IsString InfoHash where
146 fromString = either (error . prettyConvertError) id . safeConvert . T.pack
147
148ignoreErrorMsg :: Either a b -> Maybe b
149ignoreErrorMsg = either (const Nothing) Just
150
151-- | Tries both base16 and base32 while decoding info hash.
152--
153-- Use 'safeConvert' for detailed error messages.
154--
155textToInfoHash :: Text -> Maybe InfoHash
156textToInfoHash = ignoreErrorMsg . safeConvert
157
158-- | Hex encode infohash to text, full length.
159longHex :: InfoHash -> Text
160longHex = T.decodeUtf8 . Base16.encode . getInfoHash
161
162-- | The same as 'longHex', but only first 7 characters.
163shortHex :: InfoHash -> Text
164shortHex = 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 #-}
20module 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
64import Control.Applicative
65import Control.DeepSeq
66import Control.Lens
67import Data.BEncode
68import Data.BEncode.Types
69import Data.ByteString as BS
70import Data.ByteString.Base16 as Base16
71import Data.ByteString.Char8 as BC
72import Data.Default
73import Data.Foldable as F
74import Data.List as L
75import Data.Text as T
76import Data.Text.Encoding as T
77import Data.Typeable
78import Text.PrettyPrint as PP
79import Text.PrettyPrint.Class
80import System.FilePath
81import System.Posix.Types
82
83{-----------------------------------------------------------------------
84-- File attribytes
85-----------------------------------------------------------------------}
86
87-- | Size of a file in bytes.
88type FileSize = FileOffset
89
90deriving 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.
97data 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
122makeLensesFor
123 [ ("fiLength", "fileLength")
124 , ("fiMD5Sum", "fileMD5Sum")
125 , ("fiName" , "filePath" )
126 ]
127 ''FileInfo
128
129instance NFData a => NFData (FileInfo a) where
130 rnf FileInfo {..} = rnf fiName
131 {-# INLINE rnf #-}
132
133instance 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
147type Put a = a -> BDict -> BDict
148
149putFileInfoSingle :: Put (FileInfo ByteString)
150putFileInfoSingle FileInfo {..} cont =
151 "length" .=! fiLength
152 .: "md5sum" .=? fiMD5Sum
153 .: "name" .=! fiName
154 .: cont
155
156getFileInfoSingle :: Get (FileInfo ByteString)
157getFileInfoSingle = do
158 FileInfo <$>! "length"
159 <*>? "md5sum"
160 <*>! "name"
161
162instance BEncode (FileInfo ByteString) where
163 toBEncode = toDict . (`putFileInfoSingle` endDict)
164 {-# INLINE toBEncode #-}
165
166 fromBEncode = fromDict getFileInfoSingle
167 {-# INLINE fromBEncode #-}
168
169instance 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.
178joinFilePath :: FileInfo [ByteString] -> FileInfo ByteString
179joinFilePath = 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--
191data 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
205makeLensesFor
206 [ ("liFile" , "singleFile" )
207 , ("liFiles" , "multiFile" )
208 , ("liDirName", "rootDirName")
209 ]
210 ''LayoutInfo
211
212instance NFData LayoutInfo where
213 rnf SingleFile {..} = ()
214 rnf MultiFile {..} = rnf liFiles
215
216-- | Empty multifile layout.
217instance Default LayoutInfo where
218 def = MultiFile [] ""
219
220getLayoutInfo :: Get LayoutInfo
221getLayoutInfo = single <|> multi
222 where
223 single = SingleFile <$> getFileInfoSingle
224 multi = MultiFile <$>! "files" <*>! "name"
225
226putLayoutInfo :: Put LayoutInfo
227putLayoutInfo SingleFile {..} = putFileInfoSingle liFile
228putLayoutInfo MultiFile {..} = \ cont ->
229 "files" .=! liFiles
230 .: "name" .=! liDirName
231 .: cont
232
233instance BEncode LayoutInfo where
234 toBEncode = toDict . (`putLayoutInfo` endDict)
235 fromBEncode = fromDict getLayoutInfo
236
237instance 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.
242isSingleFile :: LayoutInfo -> Bool
243isSingleFile SingleFile {} = True
244isSingleFile _ = False
245{-# INLINE isSingleFile #-}
246
247-- | Test if this is multifile torrent.
248isMultiFile :: LayoutInfo -> Bool
249isMultiFile MultiFile {} = True
250isMultiFile _ = False
251{-# INLINE isMultiFile #-}
252
253-- | Get name of the torrent based on the root path piece.
254suggestedName :: LayoutInfo -> ByteString
255suggestedName (SingleFile FileInfo {..}) = fiName
256suggestedName MultiFile {..} = liDirName
257{-# INLINE suggestedName #-}
258
259-- | Find sum of sizes of the all torrent files.
260contentLength :: LayoutInfo -> FileSize
261contentLength SingleFile { liFile = FileInfo {..} } = fiLength
262contentLength MultiFile { liFiles = tfs } = L.sum (L.map fiLength tfs)
263
264-- | Get number of all files in torrent.
265fileCount :: LayoutInfo -> Int
266fileCount SingleFile {..} = 1
267fileCount 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.
271blockCount :: Int -> LayoutInfo -> Int
272blockCount 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--
283type FileLayout a = [(FilePath, a)]
284
285-- | Extract files layout from torrent info with the given root path.
286flatLayout
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.
290flatLayout prefixPath SingleFile { liFile = FileInfo {..} }
291 = [(prefixPath </> BC.unpack fiName, fiLength)]
292flatLayout 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.
300accumPositions :: FileLayout FileSize -> FileLayout (FileOffset, FileSize)
301accumPositions = 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.
307fileOffset :: FilePath -> FileLayout FileOffset -> Maybe FileOffset
308fileOffset = lookup
309{-# INLINE fileOffset #-}
310
311{-----------------------------------------------------------------------
312-- Internal utilities
313-----------------------------------------------------------------------}
314
315-- | Divide and round up.
316sizeInBase :: Integral a => a -> Int -> Int
317sizeInBase 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 #-}
29module 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
57import Control.Applicative
58import Control.Monad
59import Data.ByteString.Char8 as BC
60import Data.Convertible
61import Data.Default
62import Data.Map as M
63import Data.Maybe
64import Data.List as L
65import Data.String
66import Data.Text as T
67import Data.Text.Encoding as T
68import Data.Text.Read
69import Data.Typeable
70import Network.HTTP.Types.QueryLike
71import Network.HTTP.Types.URI
72import Network.URI
73import Text.PrettyPrint as PP
74import Text.PrettyPrint.Class
75
76import Data.Torrent
77import Data.Torrent.InfoHash
78import Data.Torrent.Layout
79
80
81-- | Namespace identifier determines the syntactic interpretation of
82-- namespace-specific string.
83type 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--
89btih :: NamespaceId
90btih = ["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--
97data 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
107instance 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--
115infohashURN :: InfoHash -> URN
116infohashURN = URN btih . longHex
117
118-- | Meaningless placeholder value.
119instance Default URN where
120 def = infohashURN def
121
122{-----------------------------------------------------------------------
123-- URN Rendering
124-----------------------------------------------------------------------}
125
126-- | Render URN to its text representation.
127renderURN :: URN -> Text
128renderURN URN {..}
129 = T.intercalate ":" $ "urn" : urnNamespace ++ [urnString]
130
131instance Pretty URN where
132 pretty = text . T.unpack . renderURN
133
134instance Show URN where
135 showsPrec n = showsPrec n . T.unpack . renderURN
136
137instance QueryValueLike URN where
138 toQueryValue = toQueryValue . renderURN
139 {-# INLINE toQueryValue #-}
140
141{-----------------------------------------------------------------------
142-- URN Parsing
143-----------------------------------------------------------------------}
144
145unsnoc :: [a] -> Maybe ([a], a)
146unsnoc [] = Nothing
147unsnoc xs = Just (L.init xs, L.last xs)
148
149instance 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
162instance 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--
170parseURN :: Text -> Maybe URN
171parseURN = 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.
181data 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
214instance QueryValueLike Integer where
215 toQueryValue = toQueryValue . show
216
217instance QueryValueLike URI where
218 toQueryValue = toQueryValue . show
219
220instance 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
232instance QueryValueLike Magnet where
233 toQueryValue = toQueryValue . renderMagnet
234
235instance 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
256magnetScheme :: URI
257magnetScheme = URI
258 { uriScheme = "magnet:"
259 , uriAuthority = Nothing
260 , uriPath = ""
261 , uriQuery = ""
262 , uriFragment = ""
263 }
264
265isMagnetURI :: URI -> Bool
266isMagnetURI u = u { uriQuery = "" } == magnetScheme
267
268-- | Can be used instead of 'parseMagnet'.
269instance 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'.
275instance Convertible Magnet URI where
276 safeConvert m = pure $ magnetScheme
277 { uriQuery = BC.unpack $ renderQuery True $ toQuery m }
278
279instance 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.
289instance 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.
303nullMagnet :: InfoHash -> Magnet
304nullMagnet 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).
317simpleMagnet :: Torrent -> Magnet
318simpleMagnet 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--
326detailedMagnet :: Torrent -> Magnet
327detailedMagnet 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
337parseMagnetStr :: String -> Maybe Magnet
338parseMagnetStr = either (const Nothing) Just . safeConvert
339
340renderMagnetStr :: Magnet -> String
341renderMagnetStr = show . (convert :: Magnet -> URI)
342
343instance Pretty Magnet where
344 pretty = PP.text . renderMagnetStr
345
346instance Show Magnet where
347 show = renderMagnetStr
348 {-# INLINE show #-}
349
350instance Read Magnet where
351 readsPrec _ xs
352 | Just m <- parseMagnetStr mstr = [(m, rest)]
353 | otherwise = []
354 where
355 (mstr, rest) = L.break (== ' ') xs
356
357instance 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--
365parseMagnet :: Text -> Maybe Magnet
366parseMagnet = parseMagnetStr . T.unpack
367{-# INLINE parseMagnet #-}
368
369-- | Render magnet link to urlencoded string
370renderMagnet :: Magnet -> Text
371renderMagnet = 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 #-}
14module 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
47import Control.DeepSeq
48import Control.Lens
49import qualified Crypto.Hash.SHA1 as SHA1
50import Data.BEncode
51import Data.BEncode.Types
52import Data.Bits
53import Data.Bits.Extras
54import Data.ByteString as BS
55import qualified Data.ByteString.Lazy as BL
56import qualified Data.ByteString.Base64 as Base64
57import Data.Default
58import Data.Int
59import Data.Text.Encoding as T
60import Data.Typeable
61import Text.PrettyPrint
62import Text.PrettyPrint.Class
63
64
65-- TODO add torrent file validation
66class 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.
77type 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--
85type PieceSize = Int
86
87-- | Number of pieces in torrent or a part of torrent.
88type PieceCount = Int
89
90defaultBlockSize :: Int
91defaultBlockSize = 16 * 1024
92
93-- | Optimal number of pieces in torrent.
94optimalPieceCount :: PieceCount
95optimalPieceCount = 1000
96{-# INLINE optimalPieceCount #-}
97
98-- | Piece size should not be less than this value.
99minPieceSize :: Int
100minPieceSize = defaultBlockSize * 4
101{-# INLINE minPieceSize #-}
102
103-- | To prevent transfer degradation piece size should not exceed this
104-- value.
105maxPieceSize :: Int
106maxPieceSize = 4 * 1024 * 1024
107{-# INLINE maxPieceSize #-}
108
109toPow2 :: Int -> Int
110toPow2 x = bit $ fromIntegral (leadingZeros (0 :: Int) - leadingZeros x)
111
112-- | Find the optimal piece size for a given torrent size.
113defaultPieceSize :: Int64 -> Int
114defaultPieceSize x = max minPieceSize $ min maxPieceSize $ toPow2 pc
115 where
116 pc = fromIntegral (x `div` fromIntegral optimalPieceCount)
117
118{-----------------------------------------------------------------------
119-- Piece data
120-----------------------------------------------------------------------}
121
122type PieceHash = ByteString
123
124hashsize :: Int
125hashsize = 20
126{-# INLINE hashsize #-}
127
128-- TODO check if pieceLength is power of 2
129-- | Piece payload should be strict or lazy bytestring.
130data 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
138instance NFData (Piece a)
139
140-- | Payload bytes are omitted.
141instance Pretty (Piece a) where
142 pretty Piece {..} = "Piece" <+> braces ("index" <+> "=" <+> int pieceIndex)
143
144-- | Get size of piece in bytes.
145pieceSize :: Piece BL.ByteString -> PieceSize
146pieceSize Piece {..} = fromIntegral (BL.length pieceData)
147
148-- | Get piece hash.
149hashPiece :: Piece BL.ByteString -> PieceHash
150hashPiece Piece {..} = SHA1.hashlazy pieceData
151
152{-----------------------------------------------------------------------
153-- Piece control
154-----------------------------------------------------------------------}
155
156-- | A flat array of SHA1 hash for each piece.
157newtype HashList = HashList { unHashList :: ByteString }
158 deriving (Show, Read, Eq, BEncode, Typeable)
159
160-- | Empty hash list.
161instance Default HashList where
162 def = HashList ""
163
164-- | Part of torrent file used for torrent content validation.
165data 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.
174makeLensesFor [("piPieceLength", "pieceLength")] ''PieceInfo
175
176-- | Concatenation of all 20-byte SHA1 hash values.
177makeLensesFor [("piPieceHashes", "pieceHashes")] ''PieceInfo
178
179instance NFData PieceInfo
180
181instance Default PieceInfo where
182 def = PieceInfo 1 def
183
184instance 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
191putPieceInfo :: PieceInfo -> BDict -> BDict
192putPieceInfo PieceInfo {..} cont =
193 "piece length" .=! piPieceLength
194 .: "pieces" .=! piPieceHashes
195 .: cont
196
197getPieceInfo :: Get PieceInfo
198getPieceInfo = do
199 PieceInfo <$>! "piece length"
200 <*>! "pieces"
201
202instance BEncode PieceInfo where
203 toBEncode = toDict . (`putPieceInfo` endDict)
204 fromBEncode = fromDict getPieceInfo
205
206-- | Hashes are omitted.
207instance Pretty PieceInfo where
208 pretty PieceInfo {..} = "Piece size: " <> int piPieceLength
209
210slice :: Int -> Int -> ByteString -> ByteString
211slice start len = BS.take len . BS.drop start
212{-# INLINE slice #-}
213
214-- | Extract validation hash by specified piece index.
215pieceHash :: PieceInfo -> PieceIx -> PieceHash
216pieceHash 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.
220pieceCount :: PieceInfo -> PieceCount
221pieceCount PieceInfo {..} = BS.length (unHashList piPieceHashes) `quot` hashsize
222
223-- | Test if this is last piece in torrent content.
224isLastPiece :: PieceInfo -> PieceIx -> Bool
225isLastPiece ci i = pieceCount ci == succ i
226
227-- | Validate piece with metainfo hash.
228checkPieceLazy :: PieceInfo -> Piece BL.ByteString -> Bool
229checkPieceLazy 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 #-}
16module 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
37import Control.Applicative
38import Control.Lens hiding ((%=))
39import Data.ByteString.Lazy.Builder as BS
40import Data.ByteString.Lazy.Builder.ASCII as BS
41import Data.Default
42import Data.List as L
43import Data.Monoid
44import Data.Serialize as S
45import Data.Ratio
46import Data.Word
47import Network.HTTP.Types.QueryLike
48import Text.PrettyPrint as PP
49import 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--
57data 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.
66instance 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
77instance Default Progress where
78 def = Progress 0 0 0
79 {-# INLINE def #-}
80
81-- | Can be used to aggregate total progress.
82instance 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
93instance QueryValueLike Builder where
94 toQueryValue = toQueryValue . BS.toLazyByteString
95
96instance QueryValueLike Word64 where
97 toQueryValue = toQueryValue . BS.word64Dec
98
99-- | HTTP Tracker protocol compatible encoding.
100instance QueryLike Progress where
101 toQuery Progress {..} =
102 [ ("uploaded" , toQueryValue _uploaded)
103 , ("left" , toQueryValue _left)
104 , ("downloaded", toQueryValue _downloaded)
105 ]
106
107instance 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--
119startProgress :: Integer -> Progress
120startProgress = Progress 0 0 . fromIntegral
121{-# INLINE startProgress #-}
122
123-- | Used when the client download some data from /any/ peer.
124downloadedProgress :: Int -> Progress -> Progress
125downloadedProgress (fromIntegral -> amount)
126 = (left -~ amount)
127 . (downloaded +~ amount)
128{-# INLINE downloadedProgress #-}
129
130-- | Used when the client upload some data to /any/ peer.
131uploadedProgress :: Int -> Progress -> Progress
132uploadedProgress (fromIntegral -> amount) = uploaded +~ amount
133{-# INLINE uploadedProgress #-}
134
135-- | Used when leecher join client session.
136enqueuedProgress :: Integer -> Progress -> Progress
137enqueuedProgress amount = left +~ fromIntegral amount
138{-# INLINE enqueuedProgress #-}
139
140-- | Used when leecher leave client session.
141-- (e.g. user deletes not completed torrent)
142dequeuedProgress :: Integer -> Progress -> Progress
143dequeuedProgress amount = left -~ fromIntegral amount
144{-# INLINE dequeuedProgress #-}
145
146ri2rw64 :: Ratio Int -> Ratio Word64
147ri2rw64 x = fromIntegral (numerator x) % fromIntegral (denominator x)
148
149-- | Check global /download/ limit by uploaded \/ downloaded ratio.
150canDownload :: Ratio Int -> Progress -> Bool
151canDownload limit Progress {..} = _uploaded % _downloaded > ri2rw64 limit
152
153-- | Check global /upload/ limit by downloaded \/ uploaded ratio.
154canUpload :: Ratio Int -> Progress -> Bool
155canUpload 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 #-}
13module 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
27import Data.ByteString as BS
28import Data.ByteString.Char8 as BC
29import Data.Foldable
30import Data.List as L
31import Data.Map as M
32import Data.Monoid
33
34import Data.Torrent.Layout
35
36
37-- | 'DirTree' is more convenient form of 'LayoutInfo'.
38data 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.
43build :: LayoutInfo -> DirTree ()
44build SingleFile {liFile = FileInfo {..}} = Dir
45 { children = M.singleton fiName (File fi) }
46 where
47 fi = FileInfo fiLength fiMD5Sum ()
48build 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.
61lookup :: [FilePath] -> DirTree a -> Maybe (DirTree a)
62lookup [] t = Just t
63lookup (p : ps) (Dir m) | Just subTree <- M.lookup (BC.pack p) m
64 = Data.Torrent.Tree.lookup ps subTree
65lookup _ _ = Nothing
66
67-- | Lookup directory by path.
68lookupDir :: [FilePath] -> DirTree a -> Maybe [(ByteString, DirTree a)]
69lookupDir 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.
76fileNumber :: DirTree a -> Sum Int
77fileNumber File {..} = Sum 1
78fileNumber Dir {..} = foldMap fileNumber children
79
80-- | Get total count of directories in the directory and subdirectories.
81dirNumber :: DirTree a -> Sum Int
82dirNumber File {..} = Sum 0
83dirNumber Dir {..} = Sum 1 <> foldMap dirNumber children