diff options
author | Sam Truzjan <pxqr.sta@gmail.com> | 2013-10-31 11:25:59 +0400 |
---|---|---|
committer | Sam Truzjan <pxqr.sta@gmail.com> | 2013-10-31 11:25:59 +0400 |
commit | 01cef3fafc27d39d88c94cacdcd8e204c5f66b86 (patch) | |
tree | 01040aca19e49f4e7937383fef53b8c82bcec12b /src/Data/Torrent/Piece.hs | |
parent | c1fec260f47084300ac30de2e43d52966316a2c7 (diff) |
Merge bittorrent package with torrent-content
Diffstat (limited to 'src/Data/Torrent/Piece.hs')
-rw-r--r-- | src/Data/Torrent/Piece.hs | 203 |
1 files changed, 203 insertions, 0 deletions
diff --git a/src/Data/Torrent/Piece.hs b/src/Data/Torrent/Piece.hs new file mode 100644 index 00000000..ea4e6253 --- /dev/null +++ b/src/Data/Torrent/Piece.hs | |||
@@ -0,0 +1,203 @@ | |||
1 | -- | | ||
2 | -- Copyright : (c) Sam Truzjan 2013 | ||
3 | -- License : BSD3 | ||
4 | -- Maintainer : pxqr.sta@gmail.com | ||
5 | -- Stability : experimental | ||
6 | -- Portability : portable | ||
7 | -- | ||
8 | {-# LANGUAGE TemplateHaskell #-} | ||
9 | {-# LANGUAGE DeriveDataTypeable #-} | ||
10 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} | ||
11 | module Data.Torrent.Piece | ||
12 | ( -- * Piece attributes | ||
13 | -- ** Piece size | ||
14 | PieceSize (..) | ||
15 | , defaultBlockSize -- TODO use data-default | ||
16 | , optimalPieceCount | ||
17 | , defaultPieceSize -- TODO use data-default | ||
18 | |||
19 | -- ** Piece index | ||
20 | , PieceIx | ||
21 | |||
22 | -- * Piece data | ||
23 | , Piece (..) | ||
24 | , ppPiece | ||
25 | |||
26 | -- * Piece control | ||
27 | , PieceInfo (..) | ||
28 | , ppPieceInfo | ||
29 | , pieceLength | ||
30 | , pieceHashes | ||
31 | , pieceHash | ||
32 | , pieceCount | ||
33 | , checkPieceLazy | ||
34 | |||
35 | |||
36 | -- * Internal | ||
37 | , getPieceInfo | ||
38 | , putPieceInfo | ||
39 | ) where | ||
40 | |||
41 | import Control.DeepSeq | ||
42 | import Control.Lens | ||
43 | import qualified Crypto.Hash.SHA1 as SHA1 | ||
44 | import Data.Aeson (ToJSON, FromJSON) | ||
45 | import Data.Aeson.TH | ||
46 | import Data.BEncode | ||
47 | import Data.BEncode.Types | ||
48 | import Data.Bits | ||
49 | import Data.Bits.Extras | ||
50 | import Data.ByteString as BS | ||
51 | import qualified Data.ByteString.Lazy as BL | ||
52 | import Data.Char | ||
53 | import Data.Int | ||
54 | import Data.List as L | ||
55 | import Data.Typeable | ||
56 | import Text.PrettyPrint | ||
57 | |||
58 | |||
59 | class Lint a where | ||
60 | lint :: a -> Either String a | ||
61 | |||
62 | type PieceCount = Int -- TODO newtype | ||
63 | type PieceIx = Int -- TODO remove | ||
64 | |||
65 | newtype PieceIndex = PieceIndex Int | ||
66 | |||
67 | -- | An int used to denote piece size. | ||
68 | newtype PieceSize = PieceSize Int | ||
69 | deriving (Show, Read, Typeable | ||
70 | , Eq, Ord, Enum | ||
71 | , Num, Real, Integral | ||
72 | , BEncode, ToJSON, FromJSON | ||
73 | ) | ||
74 | |||
75 | -- | Widely used semi-official block size. | ||
76 | defaultBlockSize :: Int | ||
77 | defaultBlockSize = 16 * 1024 | ||
78 | |||
79 | maxPieceSize :: Int | ||
80 | maxPieceSize = 4 * 1024 * 1024 | ||
81 | {-# INLINE maxPieceSize #-} | ||
82 | |||
83 | minPieceSize :: Int | ||
84 | minPieceSize = defaultBlockSize * 4 | ||
85 | {-# INLINE minPieceSize #-} | ||
86 | |||
87 | -- | NOTE: Have max and min size constrained to wide used | ||
88 | -- semi-standard values. This bounds should be used to make decision | ||
89 | -- about piece size for new torrents. | ||
90 | -- | ||
91 | instance Bounded PieceSize where | ||
92 | maxBound = PieceSize maxPieceSize | ||
93 | {-# INLINE maxBound #-} | ||
94 | |||
95 | minBound = PieceSize minPieceSize | ||
96 | {-# INLINE minBound #-} | ||
97 | |||
98 | -- | TODO | ||
99 | optimalPieceCount :: Int | ||
100 | optimalPieceCount = 1000 | ||
101 | {-# INLINE optimalPieceCount #-} | ||
102 | |||
103 | toPow2 :: Int -> Int | ||
104 | toPow2 x = bit $ fromIntegral (leadingZeros (0 :: Int) - leadingZeros x) | ||
105 | |||
106 | -- | Find the optimal piece size for a given torrent size. | ||
107 | defaultPieceSize :: Int64 -> Int | ||
108 | defaultPieceSize x = max minPieceSize $ min maxPieceSize $ toPow2 pc | ||
109 | where | ||
110 | pc = fromIntegral (x `div` fromIntegral optimalPieceCount) | ||
111 | |||
112 | -- TODO check if pieceLength is power of 2 | ||
113 | -- | Piece payload should be strict or lazy bytestring. | ||
114 | data Piece a = Piece | ||
115 | { -- | Zero-based piece index in torrent. TODO how pieces are indexed? | ||
116 | pieceIndex :: {-# UNPACK #-} !PieceIx | ||
117 | -- | Payload. | ||
118 | , pieceData :: !a | ||
119 | } deriving (Show, Read, Eq, Typeable) | ||
120 | |||
121 | $(deriveJSON (L.map toLower . L.dropWhile isLower) ''Piece) | ||
122 | |||
123 | instance NFData (Piece a) | ||
124 | |||
125 | -- | Format piece in human readable form. Payload bytes are omitted. | ||
126 | ppPiece :: Piece a -> Doc | ||
127 | ppPiece Piece {..} | ||
128 | = "Piece" <+> braces ("index" <+> "=" <+> int pieceIndex) | ||
129 | |||
130 | data PieceInfo = PieceInfo | ||
131 | { piPieceLength :: {-# UNPACK #-} !PieceSize | ||
132 | -- ^ Number of bytes in each piece. | ||
133 | |||
134 | , piPieceHashes :: !ByteString | ||
135 | -- ^ Concatenation of all 20-byte SHA1 hash values. | ||
136 | } deriving (Show, Read, Eq, Typeable) | ||
137 | |||
138 | $(deriveJSON (L.map toLower . L.dropWhile isLower) ''PieceInfo) | ||
139 | |||
140 | -- | Number of bytes in each piece. | ||
141 | makeLensesFor [("piPieceLength", "pieceLength")] ''PieceInfo | ||
142 | |||
143 | -- | Concatenation of all 20-byte SHA1 hash values. | ||
144 | makeLensesFor [("piPieceHashes", "pieceHashes")] ''PieceInfo | ||
145 | |||
146 | instance NFData PieceInfo | ||
147 | |||
148 | instance Lint PieceInfo where | ||
149 | lint pinfo @ PieceInfo {..} | ||
150 | | BS.length piPieceHashes `rem` hashsize == 0 | ||
151 | , piPieceLength >= 0 = return pinfo | ||
152 | | otherwise = Left undefined | ||
153 | |||
154 | |||
155 | putPieceInfo :: PieceInfo -> BDict -> BDict | ||
156 | putPieceInfo PieceInfo {..} cont = | ||
157 | "piece length" .=! piPieceLength | ||
158 | .: "pieces" .=! piPieceHashes | ||
159 | .: cont | ||
160 | |||
161 | getPieceInfo :: Get PieceInfo | ||
162 | getPieceInfo = do | ||
163 | PieceInfo <$>! "piece length" | ||
164 | <*>! "pieces" | ||
165 | |||
166 | instance BEncode PieceInfo where | ||
167 | toBEncode = toDict . (`putPieceInfo` endDict) | ||
168 | fromBEncode = fromDict getPieceInfo | ||
169 | |||
170 | -- | Format piece info in human readable form. Hashes are omitted. | ||
171 | ppPieceInfo :: PieceInfo -> Doc | ||
172 | ppPieceInfo PieceInfo { piPieceLength = PieceSize len } = | ||
173 | "PieceInfo" <+> braces ("length" <+> "=" <+> int len) | ||
174 | |||
175 | hashsize :: Int | ||
176 | hashsize = 20 | ||
177 | {-# INLINE hashsize #-} | ||
178 | |||
179 | slice :: Int -> Int -> ByteString -> ByteString | ||
180 | slice start len = BS.take len . BS.drop start | ||
181 | {-# INLINE slice #-} | ||
182 | |||
183 | -- | Extract validation hash by specified piece index. | ||
184 | pieceHash :: PieceInfo -> PieceIx -> ByteString | ||
185 | pieceHash PieceInfo {..} i = slice (hashsize * i) hashsize piPieceHashes | ||
186 | |||
187 | -- | Find count of pieces in the torrent. If torrent size is not a | ||
188 | -- multiple of piece size then the count is rounded up. | ||
189 | pieceCount :: PieceInfo -> PieceCount | ||
190 | pieceCount PieceInfo {..} = BS.length piPieceHashes `quot` hashsize | ||
191 | |||
192 | isLastPiece :: PieceInfo -> PieceIx -> Bool | ||
193 | isLastPiece ci i = pieceCount ci == succ i | ||
194 | |||
195 | class Validation a where | ||
196 | validate :: PieceInfo -> Piece a -> Bool | ||
197 | |||
198 | -- | Validate piece with metainfo hash. | ||
199 | checkPieceLazy :: PieceInfo -> Piece BL.ByteString -> Bool | ||
200 | checkPieceLazy pinfo @ PieceInfo {..} Piece {..} | ||
201 | = (fromIntegral (BL.length pieceData) == piPieceLength | ||
202 | || isLastPiece pinfo pieceIndex) | ||
203 | && SHA1.hashlazy pieceData == pieceHash pinfo pieceIndex | ||