summaryrefslogtreecommitdiff
path: root/src/Data/Torrent.hs
diff options
context:
space:
mode:
Diffstat (limited to 'src/Data/Torrent.hs')
-rw-r--r--src/Data/Torrent.hs270
1 files changed, 269 insertions, 1 deletions
diff --git a/src/Data/Torrent.hs b/src/Data/Torrent.hs
index 5efff598..701da9dd 100644
--- a/src/Data/Torrent.hs
+++ b/src/Data/Torrent.hs
@@ -23,7 +23,11 @@
23{-# LANGUAGE MultiParamTypeClasses #-} 23{-# LANGUAGE MultiParamTypeClasses #-}
24{-# LANGUAGE BangPatterns #-} 24{-# LANGUAGE BangPatterns #-}
25{-# LANGUAGE GeneralizedNewtypeDeriving #-} 25{-# LANGUAGE GeneralizedNewtypeDeriving #-}
26{-# LANGUAGE StandaloneDeriving #-}
26{-# LANGUAGE DeriveDataTypeable #-} 27{-# LANGUAGE DeriveDataTypeable #-}
28{-# LANGUAGE DeriveFunctor #-}
29{-# LANGUAGE DeriveFoldable #-}
30{-# LANGUAGE DeriveTraversable #-}
27{-# LANGUAGE TemplateHaskell #-} 31{-# LANGUAGE TemplateHaskell #-}
28{-# OPTIONS -fno-warn-orphans #-} 32{-# OPTIONS -fno-warn-orphans #-}
29module Data.Torrent 33module Data.Torrent
@@ -51,6 +55,37 @@ module Data.Torrent
51 , parseURN 55 , parseURN
52 , renderURN 56 , renderURN
53 57
58 -- * File layout
59 -- ** FileInfo
60 , FileOffset
61 , FileSize
62 , FileInfo (..)
63 , fileLength
64 , filePath
65 , fileMD5Sum
66
67 -- ** Layout info
68 , LayoutInfo (..)
69 , joinFilePath
70 , singleFile
71 , multiFile
72 , rootDirName
73 , isSingleFile
74 , isMultiFile
75 , suggestedName
76 , contentLength
77 , fileCount
78 , blockCount
79
80 -- ** Flat layout info
81 , FileLayout
82 , flatLayout
83 , accumPositions
84 , fileOffset
85
86 -- ** Internal
87 , sizeInBase
88
54 -- * Info dictionary 89 -- * Info dictionary
55 , InfoDict (..) 90 , InfoDict (..)
56 , infoDictionary 91 , infoDictionary
@@ -109,6 +144,7 @@ import qualified Data.ByteString.Lazy as BL
109import Data.Char 144import Data.Char
110import Data.Convertible 145import Data.Convertible
111import Data.Default 146import Data.Default
147import Data.Foldable as F
112import Data.Hashable as Hashable 148import Data.Hashable as Hashable
113import qualified Data.List as L 149import qualified Data.List as L
114import Data.Map as M 150import Data.Map as M
@@ -128,8 +164,8 @@ import Text.ParserCombinators.ReadP as P
128import Text.PrettyPrint as PP 164import Text.PrettyPrint as PP
129import Text.PrettyPrint.Class 165import Text.PrettyPrint.Class
130import System.FilePath 166import System.FilePath
167import System.Posix.Types
131 168
132import Data.Torrent.Layout
133import Data.Torrent.Piece 169import Data.Torrent.Piece
134import Network.BitTorrent.Core.NodeInfo 170import Network.BitTorrent.Core.NodeInfo
135 171
@@ -259,6 +295,238 @@ shortHex :: InfoHash -> Text
259shortHex = T.take 7 . longHex 295shortHex = T.take 7 . longHex
260 296
261{----------------------------------------------------------------------- 297{-----------------------------------------------------------------------
298-- File info
299-----------------------------------------------------------------------}
300
301-- | Size of a file in bytes.
302type FileSize = FileOffset
303
304deriving instance BEncode FileOffset
305
306-- | Contain metainfo about one single file.
307data FileInfo a = FileInfo {
308 fiLength :: {-# UNPACK #-} !FileSize
309 -- ^ Length of the file in bytes.
310
311 -- TODO unpacked MD5 sum
312 , fiMD5Sum :: !(Maybe ByteString)
313 -- ^ 32 character long MD5 sum of the file. Used by third-party
314 -- tools, not by bittorrent protocol itself.
315
316 , fiName :: !a
317 -- ^ One or more string elements that together represent the
318 -- path and filename. Each element in the list corresponds to
319 -- either a directory name or (in the case of the last element)
320 -- the filename. For example, the file:
321 --
322 -- > "dir1/dir2/file.ext"
323 --
324 -- would consist of three string elements:
325 --
326 -- > ["dir1", "dir2", "file.ext"]
327 --
328 } deriving (Show, Read, Eq, Typeable
329 , Functor, Foldable
330 )
331
332makeLensesFor
333 [ ("fiLength", "fileLength")
334 , ("fiMD5Sum", "fileMD5Sum")
335 , ("fiName" , "filePath" )
336 ]
337 ''FileInfo
338
339instance NFData a => NFData (FileInfo a) where
340 rnf FileInfo {..} = rnf fiName
341 {-# INLINE rnf #-}
342
343instance BEncode (FileInfo [ByteString]) where
344 toBEncode FileInfo {..} = toDict $
345 "length" .=! fiLength
346 .: "md5sum" .=? fiMD5Sum
347 .: "path" .=! fiName
348 .: endDict
349 {-# INLINE toBEncode #-}
350
351 fromBEncode = fromDict $ do
352 FileInfo <$>! "length"
353 <*>? "md5sum"
354 <*>! "path"
355 {-# INLINE fromBEncode #-}
356
357type Put a = a -> BDict -> BDict
358
359putFileInfoSingle :: Data.Torrent.Put (FileInfo ByteString)
360putFileInfoSingle FileInfo {..} cont =
361 "length" .=! fiLength
362 .: "md5sum" .=? fiMD5Sum
363 .: "name" .=! fiName
364 .: cont
365
366getFileInfoSingle :: BE.Get (FileInfo ByteString)
367getFileInfoSingle = do
368 FileInfo <$>! "length"
369 <*>? "md5sum"
370 <*>! "name"
371
372instance BEncode (FileInfo ByteString) where
373 toBEncode = toDict . (`putFileInfoSingle` endDict)
374 {-# INLINE toBEncode #-}
375
376 fromBEncode = fromDict getFileInfoSingle
377 {-# INLINE fromBEncode #-}
378
379instance Pretty (FileInfo BS.ByteString) where
380 pretty FileInfo {..} =
381 "Path: " <> text (T.unpack (T.decodeUtf8 fiName))
382 $$ "Size: " <> text (show fiLength)
383 $$ maybe PP.empty ppMD5 fiMD5Sum
384 where
385 ppMD5 md5 = "MD5 : " <> text (show (Base16.encode md5))
386
387-- | Join file path.
388joinFilePath :: FileInfo [ByteString] -> FileInfo ByteString
389joinFilePath = fmap (BS.intercalate "/")
390
391{-----------------------------------------------------------------------
392-- Layout info
393-----------------------------------------------------------------------}
394
395-- | Original (found in torrent file) layout info is either:
396--
397-- * Single file with its /name/.
398--
399-- * Multiple files with its relative file /paths/.
400--
401data LayoutInfo
402 = SingleFile
403 { -- | Single file info.
404 liFile :: !(FileInfo ByteString)
405 }
406 | MultiFile
407 { -- | List of the all files that torrent contains.
408 liFiles :: ![FileInfo [ByteString]]
409
410 -- | The /suggested/ name of the root directory in which to
411 -- store all the files.
412 , liDirName :: !ByteString
413 } deriving (Show, Read, Eq, Typeable)
414
415makeLensesFor
416 [ ("liFile" , "singleFile" )
417 , ("liFiles" , "multiFile" )
418 , ("liDirName", "rootDirName")
419 ]
420 ''LayoutInfo
421
422instance NFData LayoutInfo where
423 rnf SingleFile {..} = ()
424 rnf MultiFile {..} = rnf liFiles
425
426-- | Empty multifile layout.
427instance Default LayoutInfo where
428 def = MultiFile [] ""
429
430getLayoutInfo :: BE.Get LayoutInfo
431getLayoutInfo = single <|> multi
432 where
433 single = SingleFile <$> getFileInfoSingle
434 multi = MultiFile <$>! "files" <*>! "name"
435
436putLayoutInfo :: Data.Torrent.Put LayoutInfo
437putLayoutInfo SingleFile {..} = putFileInfoSingle liFile
438putLayoutInfo MultiFile {..} = \ cont ->
439 "files" .=! liFiles
440 .: "name" .=! liDirName
441 .: cont
442
443instance BEncode LayoutInfo where
444 toBEncode = toDict . (`putLayoutInfo` endDict)
445 fromBEncode = fromDict getLayoutInfo
446
447instance Pretty LayoutInfo where
448 pretty SingleFile {..} = pretty liFile
449 pretty MultiFile {..} = vcat $ L.map (pretty . joinFilePath) liFiles
450
451-- | Test if this is single file torrent.
452isSingleFile :: LayoutInfo -> Bool
453isSingleFile SingleFile {} = True
454isSingleFile _ = False
455{-# INLINE isSingleFile #-}
456
457-- | Test if this is multifile torrent.
458isMultiFile :: LayoutInfo -> Bool
459isMultiFile MultiFile {} = True
460isMultiFile _ = False
461{-# INLINE isMultiFile #-}
462
463-- | Get name of the torrent based on the root path piece.
464suggestedName :: LayoutInfo -> ByteString
465suggestedName (SingleFile FileInfo {..}) = fiName
466suggestedName MultiFile {..} = liDirName
467{-# INLINE suggestedName #-}
468
469-- | Find sum of sizes of the all torrent files.
470contentLength :: LayoutInfo -> FileSize
471contentLength SingleFile { liFile = FileInfo {..} } = fiLength
472contentLength MultiFile { liFiles = tfs } = L.sum (L.map fiLength tfs)
473
474-- | Get number of all files in torrent.
475fileCount :: LayoutInfo -> Int
476fileCount SingleFile {..} = 1
477fileCount MultiFile {..} = L.length liFiles
478
479-- | Find number of blocks of the specified size. If torrent size is
480-- not a multiple of block size then the count is rounded up.
481blockCount :: Int -> LayoutInfo -> Int
482blockCount blkSize ci = contentLength ci `sizeInBase` blkSize
483
484------------------------------------------------------------------------
485
486-- | File layout specifies the order and the size of each file in the
487-- storage. Note that order of files is highly important since we
488-- coalesce all the files in the given order to get the linear block
489-- address space.
490--
491type FileLayout a = [(FilePath, a)]
492
493-- | Extract files layout from torrent info with the given root path.
494flatLayout
495 :: FilePath -- ^ Root path for the all torrent files.
496 -> LayoutInfo -- ^ Torrent content information.
497 -> FileLayout FileSize -- ^ The all file paths prefixed with the given root.
498flatLayout prefixPath SingleFile { liFile = FileInfo {..} }
499 = [(prefixPath </> BC.unpack fiName, fiLength)]
500flatLayout prefixPath MultiFile {..} = L.map mkPath liFiles
501 where -- TODO use utf8 encoding in name
502 mkPath FileInfo {..} = (path, fiLength)
503 where
504 path = prefixPath </> BC.unpack liDirName
505 </> joinPath (L.map BC.unpack fiName)
506
507-- | Calculate offset of each file based on its length, incrementally.
508accumPositions :: FileLayout FileSize -> FileLayout (FileOffset, FileSize)
509accumPositions = go 0
510 where
511 go !_ [] = []
512 go !offset ((n, s) : xs) = (n, (offset, s)) : go (offset + s) xs
513
514-- | Gives global offset of a content file for a given full path.
515fileOffset :: FilePath -> FileLayout FileOffset -> Maybe FileOffset
516fileOffset = L.lookup
517{-# INLINE fileOffset #-}
518
519------------------------------------------------------------------------
520
521-- | Divide and round up.
522sizeInBase :: Integral a => a -> Int -> Int
523sizeInBase n b = fromIntegral (n `div` fromIntegral b) + align
524 where
525 align = if n `mod` fromIntegral b == 0 then 0 else 1
526{-# SPECIALIZE sizeInBase :: Int -> Int -> Int #-}
527{-# SPECIALIZE sizeInBase :: Integer -> Int -> Int #-}
528
529{-----------------------------------------------------------------------
262-- Info dictionary 530-- Info dictionary
263-----------------------------------------------------------------------} 531-----------------------------------------------------------------------}
264 532