diff options
author | Sam Truzjan <pxqr.sta@gmail.com> | 2013-11-28 06:28:06 +0400 |
---|---|---|
committer | Sam Truzjan <pxqr.sta@gmail.com> | 2013-11-28 06:28:06 +0400 |
commit | fc4ed85a313f93b4fb0c46b500ee12c38e94df68 (patch) | |
tree | 3eed36a608878fe5d232ab5d84df1e4ffefed24a | |
parent | 533068e7ebbf3ae5f15bd7b65312a69ab50052e5 (diff) |
Implement AnnounceQuery parsing
-rw-r--r-- | src/Data/Torrent/InfoHash.hs | 20 | ||||
-rw-r--r-- | src/Network/BitTorrent/Core/PeerId.hs | 18 | ||||
-rw-r--r-- | src/Network/BitTorrent/Tracker/Message.hs | 108 | ||||
-rw-r--r-- | tests/Network/BitTorrent/Core/PeerIdSpec.hs | 9 | ||||
-rw-r--r-- | tests/Network/BitTorrent/Tracker/MessageSpec.hs | 12 |
5 files changed, 108 insertions, 59 deletions
diff --git a/src/Data/Torrent/InfoHash.hs b/src/Data/Torrent/InfoHash.hs index 3d2bbe25..a4d6a02e 100644 --- a/src/Data/Torrent/InfoHash.hs +++ b/src/Data/Torrent/InfoHash.hs | |||
@@ -14,6 +14,7 @@ module Data.Torrent.InfoHash | |||
14 | InfoHash(..) | 14 | InfoHash(..) |
15 | 15 | ||
16 | -- * Parsing | 16 | -- * Parsing |
17 | , byteStringToInfoHash | ||
17 | , textToInfoHash | 18 | , textToInfoHash |
18 | 19 | ||
19 | -- * Rendering | 20 | -- * Rendering |
@@ -54,6 +55,16 @@ import Text.PrettyPrint | |||
54 | import Text.PrettyPrint.Class | 55 | import Text.PrettyPrint.Class |
55 | 56 | ||
56 | 57 | ||
58 | -- TODO | ||
59 | -- | ||
60 | -- data Word160 = Word160 {-# UNPACK #-} !Word64 | ||
61 | -- {-# UNPACK #-} !Word64 | ||
62 | -- {-# UNPACK #-} !Word32 | ||
63 | -- | ||
64 | -- newtype InfoHash = InfoHash Word160 | ||
65 | -- | ||
66 | -- reason: bytestring have overhead = 8 words, while infohash have length 20 bytes | ||
67 | |||
57 | -- | Exactly 20 bytes long SHA1 hash of the info part of torrent file. | 68 | -- | Exactly 20 bytes long SHA1 hash of the info part of torrent file. |
58 | newtype InfoHash = InfoHash { getInfoHash :: BS.ByteString } | 69 | newtype InfoHash = InfoHash { getInfoHash :: BS.ByteString } |
59 | deriving (Eq, Ord) | 70 | deriving (Eq, Ord) |
@@ -110,6 +121,15 @@ instance URLShow InfoHash where | |||
110 | instance Pretty InfoHash where | 121 | instance Pretty InfoHash where |
111 | pretty = text . BC.unpack . ppHex . getInfoHash | 122 | pretty = text . BC.unpack . ppHex . getInfoHash |
112 | 123 | ||
124 | infoHashLen :: Int | ||
125 | infoHashLen = 20 | ||
126 | |||
127 | -- | Convert raw bytes to info hash. | ||
128 | byteStringToInfoHash :: BS.ByteString -> Maybe InfoHash | ||
129 | byteStringToInfoHash bs | ||
130 | | BS.length bs == infoHashLen = Just (InfoHash bs) | ||
131 | | otherwise = Nothing | ||
132 | |||
113 | -- | Tries both base16 and base32 while decoding info hash. | 133 | -- | Tries both base16 and base32 while decoding info hash. |
114 | textToInfoHash :: Text -> Maybe InfoHash | 134 | textToInfoHash :: Text -> Maybe InfoHash |
115 | textToInfoHash t | 135 | textToInfoHash t |
diff --git a/src/Network/BitTorrent/Core/PeerId.hs b/src/Network/BitTorrent/Core/PeerId.hs index 2c79739a..a2b03e92 100644 --- a/src/Network/BitTorrent/Core/PeerId.hs +++ b/src/Network/BitTorrent/Core/PeerId.hs | |||
@@ -15,6 +15,7 @@ | |||
15 | module Network.BitTorrent.Core.PeerId | 15 | module Network.BitTorrent.Core.PeerId |
16 | ( -- * PeerId | 16 | ( -- * PeerId |
17 | PeerId (getPeerId) | 17 | PeerId (getPeerId) |
18 | , byteStringToPeerId | ||
18 | 19 | ||
19 | -- * Generation | 20 | -- * Generation |
20 | , genPeerId | 21 | , genPeerId |
@@ -60,30 +61,37 @@ import Paths_bittorrent (version) | |||
60 | 61 | ||
61 | import Data.Torrent.Client | 62 | import Data.Torrent.Client |
62 | 63 | ||
63 | -- TODO use unpacked form (length is known statically) | 64 | -- TODO use unpacked Word160 form (length is known statically) |
64 | 65 | ||
65 | -- | Peer identifier is exactly 20 bytes long bytestring. | 66 | -- | Peer identifier is exactly 20 bytes long bytestring. |
66 | newtype PeerId = PeerId { getPeerId :: ByteString } | 67 | newtype PeerId = PeerId { getPeerId :: ByteString } |
67 | deriving (Show, Eq, Ord, BEncode, ToJSON, FromJSON) | 68 | deriving (Show, Eq, Ord, BEncode, ToJSON, FromJSON) |
68 | 69 | ||
70 | peerIdLen :: Int | ||
71 | peerIdLen = 20 | ||
72 | |||
69 | instance Serialize PeerId where | 73 | instance Serialize PeerId where |
70 | put = putByteString . getPeerId | 74 | put = putByteString . getPeerId |
71 | get = PeerId <$> getBytes 20 | 75 | get = PeerId <$> getBytes peerIdLen |
72 | 76 | ||
73 | instance URLShow PeerId where | 77 | instance URLShow PeerId where |
74 | urlShow = BC.unpack . getPeerId | 78 | urlShow = BC.unpack . getPeerId |
75 | 79 | ||
76 | instance IsString PeerId where | 80 | instance IsString PeerId where |
77 | fromString str | 81 | fromString str |
78 | | BS.length bs == 20 = PeerId bs | 82 | | BS.length bs == peerIdLen = PeerId bs |
79 | | otherwise = error $ "Peer id should be 20 bytes long: " | 83 | | otherwise = error $ "Peer id should be 20 bytes long: " ++ show str |
80 | ++ show str | ||
81 | where | 84 | where |
82 | bs = fromString str | 85 | bs = fromString str |
83 | 86 | ||
84 | instance Pretty PeerId where | 87 | instance Pretty PeerId where |
85 | pretty = text . BC.unpack . getPeerId | 88 | pretty = text . BC.unpack . getPeerId |
86 | 89 | ||
90 | byteStringToPeerId :: BS.ByteString -> Maybe PeerId | ||
91 | byteStringToPeerId bs | ||
92 | | BS.length bs == peerIdLen = Just (PeerId bs) | ||
93 | | otherwise = Nothing | ||
94 | |||
87 | {----------------------------------------------------------------------- | 95 | {----------------------------------------------------------------------- |
88 | -- Encoding | 96 | -- Encoding |
89 | -----------------------------------------------------------------------} | 97 | -----------------------------------------------------------------------} |
diff --git a/src/Network/BitTorrent/Tracker/Message.hs b/src/Network/BitTorrent/Tracker/Message.hs index 3447a367..59ef2027 100644 --- a/src/Network/BitTorrent/Tracker/Message.hs +++ b/src/Network/BitTorrent/Tracker/Message.hs | |||
@@ -52,6 +52,8 @@ import Data.Aeson (ToJSON(..), FromJSON(..)) | |||
52 | import Data.Aeson.TH | 52 | import Data.Aeson.TH |
53 | import Data.BEncode as BE | 53 | import Data.BEncode as BE |
54 | import Data.BEncode.BDict as BE | 54 | import Data.BEncode.BDict as BE |
55 | import Data.ByteString as BS | ||
56 | import Data.ByteString.Char8 as BC | ||
55 | import Data.Char as Char | 57 | import Data.Char as Char |
56 | import Data.List as L | 58 | import Data.List as L |
57 | import Data.Map as M | 59 | import Data.Map as M |
@@ -64,8 +66,10 @@ import Data.Typeable | |||
64 | import Data.URLEncoded as URL | 66 | import Data.URLEncoded as URL |
65 | import Data.Word | 67 | import Data.Word |
66 | import Network | 68 | import Network |
67 | import Network.URI | 69 | import Network.HTTP.Types.URI hiding (urlEncode) |
68 | import Network.Socket | 70 | import Network.Socket |
71 | import Network.URI | ||
72 | import Text.Read (readMaybe) | ||
69 | 73 | ||
70 | import Data.Torrent.InfoHash | 74 | import Data.Torrent.InfoHash |
71 | import Data.Torrent.Progress | 75 | import Data.Torrent.Progress |
@@ -226,63 +230,83 @@ data QueryParam | |||
226 | = ParamInfoHash | 230 | = ParamInfoHash |
227 | | ParamPeerId | 231 | | ParamPeerId |
228 | | ParamPort | 232 | | ParamPort |
229 | | ParamProgress | 233 | | ParamUploaded |
234 | | ParamLeft | ||
235 | | ParamDownloaded | ||
230 | | ParamIP | 236 | | ParamIP |
231 | | ParamNumWant | 237 | | ParamNumWant |
232 | | ParamEvent | 238 | | ParamEvent |
233 | deriving (Show, Eq, Ord, Enum) | 239 | deriving (Show, Eq, Ord, Enum) |
234 | 240 | ||
235 | data ParamParseFailure | 241 | data ParamParseFailure |
236 | = Missing QueryParam -- ^ param not found in query string; | 242 | = Missing QueryParam -- ^ param not found in query string; |
237 | | Invalid QueryParam Text -- ^ param present but not valid. | 243 | | Invalid QueryParam ByteString -- ^ param present but not valid. |
238 | deriving (Show, Eq) | 244 | deriving (Show, Eq) |
239 | 245 | ||
240 | type ParamResult = Either ParamParseFailure | 246 | paramName :: QueryParam -> ByteString |
247 | paramName ParamInfoHash = "info_hash" | ||
248 | paramName ParamPeerId = "peer_id" | ||
249 | paramName ParamPort = "port" | ||
250 | paramName ParamUploaded = "uploaded" | ||
251 | paramName ParamLeft = "left" | ||
252 | paramName ParamDownloaded = "downloaded" | ||
253 | paramName ParamIP = "ip" | ||
254 | paramName ParamNumWant = "numwant" | ||
255 | paramName ParamEvent = "event" | ||
256 | |||
257 | class FromParam a where | ||
258 | fromParam :: BS.ByteString -> Maybe a | ||
259 | |||
260 | instance FromParam InfoHash where | ||
261 | fromParam = byteStringToInfoHash | ||
262 | |||
263 | instance FromParam PeerId where | ||
264 | fromParam = byteStringToPeerId | ||
265 | |||
266 | instance FromParam Word32 where | ||
267 | fromParam = readMaybe . BC.unpack | ||
268 | |||
269 | instance FromParam Word64 where | ||
270 | fromParam = readMaybe . BC.unpack | ||
271 | |||
272 | instance FromParam Int where | ||
273 | fromParam = readMaybe . BC.unpack | ||
241 | 274 | ||
242 | textToPeerId :: Text -> Maybe PeerId | 275 | instance FromParam PortNumber where |
243 | textToPeerId = undefined | 276 | fromParam bs = fromIntegral <$> (fromParam bs :: Maybe Word32) |
244 | 277 | ||
245 | textToPortNumber :: Text -> Maybe PortNumber | 278 | instance FromParam Event where |
246 | textToPortNumber = undefined | 279 | fromParam bs = case BC.uncons bs of |
280 | Nothing -> Nothing | ||
281 | Just (x, xs) -> readMaybe $ BC.unpack $ BC.cons (Char.toUpper x) xs | ||
247 | 282 | ||
248 | textToHostAddress :: Text -> Maybe HostAddress | 283 | withError e = maybe (Left e) Right |
249 | textToHostAddress = undefined | ||
250 | 284 | ||
251 | textToNumWant :: Text -> Maybe Int | 285 | reqParam param xs = do |
252 | textToNumWant = undefined | 286 | val <- withError (Missing param) $ L.lookup (paramName param) xs |
287 | withError (Invalid param val) (fromParam val) | ||
253 | 288 | ||
254 | textToEvent :: Text -> Maybe Event | 289 | optParam param ps |
255 | textToEvent = undefined | 290 | | Just x <- L.lookup (paramName param) ps |
291 | = pure <$> withError (Invalid param x) (fromParam x) | ||
292 | | otherwise = pure Nothing | ||
256 | 293 | ||
257 | paramName :: QueryParam -> Text | 294 | parseProgress :: SimpleQuery -> Either ParamParseFailure Progress |
258 | paramName ParamInfoHash = "info_hash" | 295 | parseProgress params = Progress |
259 | paramName ParamPeerId = "peer_id" | 296 | <$> reqParam ParamDownloaded params |
260 | paramName ParamPort = "port" | 297 | <*> reqParam ParamLeft params |
298 | <*> reqParam ParamUploaded params | ||
261 | 299 | ||
262 | -- | Parse announce request from a decoded query string. | 300 | -- | Parse announce request from a query string. |
263 | parseAnnounceQuery :: [(Text, Text)] -> Either ParamParseFailure AnnounceQuery | 301 | parseAnnounceQuery :: SimpleQuery -> Either ParamParseFailure AnnounceQuery |
264 | parseAnnounceQuery params = AnnounceQuery | 302 | parseAnnounceQuery params = AnnounceQuery |
265 | <$> reqParam ParamInfoHash textToInfoHash params | 303 | <$> reqParam ParamInfoHash params |
266 | <*> reqParam ParamPeerId textToPeerId params | 304 | <*> reqParam ParamPeerId params |
267 | <*> reqParam ParamPort textToPortNumber params | 305 | <*> reqParam ParamPort params |
268 | <*> progress params | 306 | <*> parseProgress params |
269 | <*> optParam ParamIP textToHostAddress params | 307 | <*> optParam ParamIP params |
270 | <*> optParam ParamNumWant textToNumWant params | 308 | <*> optParam ParamNumWant params |
271 | <*> optParam ParamEvent textToEvent params | 309 | <*> optParam ParamEvent params |
272 | where | ||
273 | withError e = maybe (Left e) Right | ||
274 | reqParam param p = withError (Missing param) . L.lookup (paramName param) | ||
275 | >=> \x -> withError (Invalid param x) (p x) | ||
276 | |||
277 | optParam param p ps | ||
278 | | Just x <- L.lookup (paramName param) ps | ||
279 | = pure <$> withError (Invalid param x) (p x) | ||
280 | | otherwise = pure Nothing | ||
281 | |||
282 | progress = undefined | ||
283 | ip = undefined | ||
284 | numwant = undefined | ||
285 | event = undefined | ||
286 | 310 | ||
287 | -- TODO add extension datatype | 311 | -- TODO add extension datatype |
288 | 312 | ||
diff --git a/tests/Network/BitTorrent/Core/PeerIdSpec.hs b/tests/Network/BitTorrent/Core/PeerIdSpec.hs index 7153ef68..4e9f1655 100644 --- a/tests/Network/BitTorrent/Core/PeerIdSpec.hs +++ b/tests/Network/BitTorrent/Core/PeerIdSpec.hs | |||
@@ -1,15 +1,18 @@ | |||
1 | module Network.BitTorrent.Core.PeerIdSpec (spec) where | 1 | module Network.BitTorrent.Core.PeerIdSpec (spec) where |
2 | import Control.Applicative | 2 | import Control.Applicative |
3 | import Data.Text.Encoding as T | ||
3 | import Test.Hspec | 4 | import Test.Hspec |
4 | import Test.QuickCheck | 5 | import Test.QuickCheck |
5 | import Test.QuickCheck.Instances | 6 | import Test.QuickCheck.Instances () |
6 | import Network.BitTorrent.Core.PeerId | 7 | import Network.BitTorrent.Core.PeerId |
7 | 8 | ||
8 | 9 | ||
9 | instance Arbitrary PeerId where | 10 | instance Arbitrary PeerId where |
10 | arbitrary = oneof | 11 | arbitrary = oneof |
11 | [ azureusStyle defaultClientId defaultVersionNumber <$> arbitrary | 12 | [ azureusStyle defaultClientId defaultVersionNumber |
12 | , shadowStyle 'X' defaultVersionNumber <$> arbitrary | 13 | <$> pure "" |
14 | -- , shadowStyle 'X' defaultVersionNumber | ||
15 | -- <$> (T.encodeUtf8 <$> arbitrary) | ||
13 | ] | 16 | ] |
14 | 17 | ||
15 | spec :: Spec | 18 | spec :: Spec |
diff --git a/tests/Network/BitTorrent/Tracker/MessageSpec.hs b/tests/Network/BitTorrent/Tracker/MessageSpec.hs index 8a06aaba..2a882c5d 100644 --- a/tests/Network/BitTorrent/Tracker/MessageSpec.hs +++ b/tests/Network/BitTorrent/Tracker/MessageSpec.hs | |||
@@ -44,8 +44,8 @@ instance Arbitrary AnnounceQuery where | |||
44 | baseURI :: URI | 44 | baseURI :: URI |
45 | baseURI = fromJust $ parseURI "http://a" | 45 | baseURI = fromJust $ parseURI "http://a" |
46 | 46 | ||
47 | parseUriQuery :: URI -> [(Text, Text)] | 47 | parseUriQuery :: URI -> SimpleQuery |
48 | parseUriQuery = filterMaybes . parseQueryText . BC.pack . uriQuery | 48 | parseUriQuery = filterMaybes . parseQuery . BC.pack . uriQuery |
49 | where | 49 | where |
50 | filterMaybes :: [(a, Maybe b)] -> [(a, b)] | 50 | filterMaybes :: [(a, Maybe b)] -> [(a, b)] |
51 | filterMaybes = catMaybes . L.map f | 51 | filterMaybes = catMaybes . L.map f |
@@ -53,16 +53,10 @@ parseUriQuery = filterMaybes . parseQueryText . BC.pack . uriQuery | |||
53 | f (a, Nothing) = Nothing | 53 | f (a, Nothing) = Nothing |
54 | f (a, Just b ) = Just (a, b) | 54 | f (a, Just b ) = Just (a, b) |
55 | 55 | ||
56 | test = do | ||
57 | let q = unGen arbitrary (mkStdGen 0) 0 | ||
58 | print $ renderAnnounceQuery baseURI q | ||
59 | print $ parseUriQuery $ renderAnnounceQuery baseURI q | ||
60 | |||
61 | spec :: Spec | 56 | spec :: Spec |
62 | spec = do | 57 | spec = do |
63 | describe "Announce" $ do | 58 | describe "Announce" $ do |
64 | before test $ | 59 | it "properly url encoded" $ property $ \ q -> |
65 | it "properly url encoded" $ property $ \ q -> | ||
66 | parseAnnounceQuery (parseUriQuery (renderAnnounceQuery baseURI q)) | 60 | parseAnnounceQuery (parseUriQuery (renderAnnounceQuery baseURI q)) |
67 | `shouldBe` Right q | 61 | `shouldBe` Right q |
68 | 62 | ||