diff options
author | Sam Truzjan <pxqr.sta@gmail.com> | 2014-04-04 20:30:54 +0400 |
---|---|---|
committer | Sam Truzjan <pxqr.sta@gmail.com> | 2014-04-04 20:30:54 +0400 |
commit | 7a892425de92efd88b98576e848bebc725a9bf14 (patch) | |
tree | b872eaf1bfd6cfe5d302f31f9c9b7c1a5b6d0a61 /src/Data | |
parent | 9b2981d38cfa188099cca07337a3b63747e2c527 (diff) |
Move Infohash and Magnet to Torrent module
Diffstat (limited to 'src/Data')
-rw-r--r-- | src/Data/Torrent.hs | 489 | ||||
-rw-r--r-- | src/Data/Torrent/InfoHash.hs | 164 | ||||
-rw-r--r-- | src/Data/Torrent/Magnet.hs | 372 |
3 files changed, 478 insertions, 547 deletions
diff --git a/src/Data/Torrent.hs b/src/Data/Torrent.hs index b233937b..5efff598 100644 --- a/src/Data/Torrent.hs +++ b/src/Data/Torrent.hs | |||
@@ -17,16 +17,42 @@ | |||
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 #-} |
24 | {-# LANGUAGE DeriveDataTypeable #-} | 26 | {-# LANGUAGE DeriveDataTypeable #-} |
25 | {-# LANGUAGE TemplateHaskell #-} | 27 | {-# LANGUAGE TemplateHaskell #-} |
26 | {-# OPTIONS -fno-warn-orphans #-} | 28 | {-# OPTIONS -fno-warn-orphans #-} |
27 | module Data.Torrent | 29 | module Data.Torrent |
28 | ( -- * Info dictionary | 30 | ( -- * InfoHash |
29 | InfoDict (..) | 31 | -- $infohash |
32 | InfoHash | ||
33 | , textToInfoHash | ||
34 | , longHex | ||
35 | , shortHex | ||
36 | |||
37 | -- * Magnet | ||
38 | -- $magnet-link | ||
39 | , Magnet(..) | ||
40 | , nullMagnet | ||
41 | , simpleMagnet | ||
42 | , detailedMagnet | ||
43 | , parseMagnet | ||
44 | , renderMagnet | ||
45 | |||
46 | -- ** URN | ||
47 | , URN (..) | ||
48 | , NamespaceId | ||
49 | , btih | ||
50 | , infohashURN | ||
51 | , parseURN | ||
52 | , renderURN | ||
53 | |||
54 | -- * Info dictionary | ||
55 | , InfoDict (..) | ||
30 | , infoDictionary | 56 | , infoDictionary |
31 | 57 | ||
32 | -- ** Lenses | 58 | -- ** Lenses |
@@ -67,34 +93,171 @@ module Data.Torrent | |||
67 | 93 | ||
68 | import Prelude hiding (sum) | 94 | import Prelude hiding (sum) |
69 | import Control.Applicative | 95 | import Control.Applicative |
70 | import qualified Crypto.Hash.SHA1 as C | ||
71 | import Control.DeepSeq | 96 | import Control.DeepSeq |
72 | import Control.Exception | 97 | import Control.Exception |
73 | import Control.Lens | 98 | import Control.Lens hiding (unsnoc) |
99 | import Control.Monad | ||
100 | import qualified Crypto.Hash.SHA1 as C | ||
74 | import Data.BEncode as BE | 101 | import Data.BEncode as BE |
75 | import Data.BEncode.Types as BE | 102 | import Data.BEncode.Types as BE |
76 | import Data.ByteString as BS | 103 | import Data.ByteString as BS |
104 | import Data.ByteString.Base16 as Base16 | ||
105 | import Data.ByteString.Base32 as Base32 | ||
106 | import Data.ByteString.Base64 as Base64 | ||
77 | import qualified Data.ByteString.Char8 as BC (pack, unpack) | 107 | import qualified Data.ByteString.Char8 as BC (pack, unpack) |
78 | import qualified Data.ByteString.Lazy as BL | 108 | import qualified Data.ByteString.Lazy as BL |
109 | import Data.Char | ||
79 | import Data.Convertible | 110 | import Data.Convertible |
80 | import Data.Default | 111 | import Data.Default |
81 | import Data.Hashable as Hashable | 112 | import Data.Hashable as Hashable |
82 | import qualified Data.List as L | 113 | import qualified Data.List as L |
114 | import Data.Map as M | ||
115 | import Data.Maybe | ||
116 | import Data.Serialize as S | ||
117 | import Data.String | ||
83 | import Data.Text as T | 118 | import Data.Text as T |
84 | import Data.Time | 119 | import Data.Text.Encoding as T |
120 | import Data.Text.Read | ||
85 | import Data.Time.Clock.POSIX | 121 | import Data.Time.Clock.POSIX |
86 | import Data.Typeable | 122 | import Data.Typeable |
87 | import Network (HostName) | 123 | import Network (HostName) |
124 | import Network.HTTP.Types.QueryLike | ||
125 | import Network.HTTP.Types.URI | ||
88 | import Network.URI | 126 | import Network.URI |
127 | import Text.ParserCombinators.ReadP as P | ||
89 | import Text.PrettyPrint as PP | 128 | import Text.PrettyPrint as PP |
90 | import Text.PrettyPrint.Class | 129 | import Text.PrettyPrint.Class |
91 | import System.FilePath | 130 | import System.FilePath |
92 | 131 | ||
93 | import Data.Torrent.InfoHash as IH | ||
94 | import Data.Torrent.Layout | 132 | import Data.Torrent.Layout |
95 | import Data.Torrent.Piece | 133 | import Data.Torrent.Piece |
96 | import Network.BitTorrent.Core.NodeInfo | 134 | import Network.BitTorrent.Core.NodeInfo |
97 | 135 | ||
136 | |||
137 | {----------------------------------------------------------------------- | ||
138 | -- Info hash | ||
139 | -----------------------------------------------------------------------} | ||
140 | -- TODO | ||
141 | -- | ||
142 | -- data Word160 = Word160 {-# UNPACK #-} !Word64 | ||
143 | -- {-# UNPACK #-} !Word64 | ||
144 | -- {-# UNPACK #-} !Word32 | ||
145 | -- | ||
146 | -- newtype InfoHash = InfoHash Word160 | ||
147 | -- | ||
148 | -- reason: bytestring have overhead = 8 words, while infohash have length 20 bytes | ||
149 | |||
150 | -- $infohash | ||
151 | -- | ||
152 | -- Infohash is a unique identifier of torrent. | ||
153 | |||
154 | -- | Exactly 20 bytes long SHA1 hash of the info part of torrent file. | ||
155 | newtype InfoHash = InfoHash { getInfoHash :: BS.ByteString } | ||
156 | deriving (Eq, Ord, Typeable) | ||
157 | |||
158 | infoHashLen :: Int | ||
159 | infoHashLen = 20 | ||
160 | |||
161 | -- | Meaningless placeholder value. | ||
162 | instance Default InfoHash where | ||
163 | def = "0123456789012345678901234567890123456789" | ||
164 | |||
165 | -- | Hash raw bytes. (no encoding) | ||
166 | instance Hashable InfoHash where | ||
167 | hashWithSalt s (InfoHash ih) = hashWithSalt s ih | ||
168 | {-# INLINE hashWithSalt #-} | ||
169 | |||
170 | -- | Convert to\/from raw bencoded string. (no encoding) | ||
171 | instance BEncode InfoHash where | ||
172 | toBEncode = toBEncode . getInfoHash | ||
173 | fromBEncode be = InfoHash <$> fromBEncode be | ||
174 | |||
175 | -- | Convert to\/from raw bytestring. (no encoding) | ||
176 | instance Serialize InfoHash where | ||
177 | put (InfoHash ih) = putByteString ih | ||
178 | {-# INLINE put #-} | ||
179 | |||
180 | get = InfoHash <$> getBytes infoHashLen | ||
181 | {-# INLINE get #-} | ||
182 | |||
183 | -- | Convert to raw query value. (no encoding) | ||
184 | instance QueryValueLike InfoHash where | ||
185 | toQueryValue (InfoHash ih) = Just ih | ||
186 | {-# INLINE toQueryValue #-} | ||
187 | |||
188 | -- | Convert to base16 encoded string. | ||
189 | instance Show InfoHash where | ||
190 | show (InfoHash ih) = BC.unpack (Base16.encode ih) | ||
191 | |||
192 | -- | Convert to base16 encoded Doc string. | ||
193 | instance Pretty InfoHash where | ||
194 | pretty = text . show | ||
195 | |||
196 | -- | Read base16 encoded string. | ||
197 | instance Read InfoHash where | ||
198 | readsPrec _ = readP_to_S $ do | ||
199 | str <- replicateM (infoHashLen * 2) (satisfy isHexDigit) | ||
200 | return $ InfoHash $ decodeIH str | ||
201 | where | ||
202 | decodeIH = BS.pack . L.map fromHex . pair | ||
203 | fromHex (a, b) = read $ '0' : 'x' : a : b : [] | ||
204 | |||
205 | pair (a : b : xs) = (a, b) : pair xs | ||
206 | pair _ = [] | ||
207 | |||
208 | -- | Convert raw bytes to info hash. | ||
209 | instance Convertible BS.ByteString InfoHash where | ||
210 | safeConvert bs | ||
211 | | BS.length bs == infoHashLen = pure (InfoHash bs) | ||
212 | | otherwise = convError "invalid length" bs | ||
213 | |||
214 | -- | Parse infohash from base16\/base32\/base64 encoded string. | ||
215 | instance Convertible Text InfoHash where | ||
216 | safeConvert t | ||
217 | | 20 == hashLen = pure (InfoHash hashStr) | ||
218 | | 26 <= hashLen && hashLen <= 28 = | ||
219 | case Base64.decode hashStr of | ||
220 | Left msg -> convError ("invalid base64 encoding " ++ msg) t | ||
221 | Right ihStr -> safeConvert ihStr | ||
222 | |||
223 | | hashLen == 32 = | ||
224 | case Base32.decode hashStr of | ||
225 | Left msg -> convError msg t | ||
226 | Right ihStr -> safeConvert ihStr | ||
227 | |||
228 | | hashLen == 40 = | ||
229 | let (ihStr, inv) = Base16.decode hashStr | ||
230 | in if BS.length inv /= 0 | ||
231 | then convError "invalid base16 encoding" t | ||
232 | else safeConvert ihStr | ||
233 | |||
234 | | otherwise = convError "invalid length" t | ||
235 | where | ||
236 | hashLen = BS.length hashStr | ||
237 | hashStr = T.encodeUtf8 t | ||
238 | |||
239 | -- | Decode from base16\/base32\/base64 encoded string. | ||
240 | instance IsString InfoHash where | ||
241 | fromString = either (error . prettyConvertError) id . safeConvert . T.pack | ||
242 | |||
243 | ignoreErrorMsg :: Either a b -> Maybe b | ||
244 | ignoreErrorMsg = either (const Nothing) Just | ||
245 | |||
246 | -- | Tries both base16 and base32 while decoding info hash. | ||
247 | -- | ||
248 | -- Use 'safeConvert' for detailed error messages. | ||
249 | -- | ||
250 | textToInfoHash :: Text -> Maybe InfoHash | ||
251 | textToInfoHash = ignoreErrorMsg . safeConvert | ||
252 | |||
253 | -- | Hex encode infohash to text, full length. | ||
254 | longHex :: InfoHash -> Text | ||
255 | longHex = T.decodeUtf8 . Base16.encode . getInfoHash | ||
256 | |||
257 | -- | The same as 'longHex', but only first 7 characters. | ||
258 | shortHex :: InfoHash -> Text | ||
259 | shortHex = T.take 7 . longHex | ||
260 | |||
98 | {----------------------------------------------------------------------- | 261 | {----------------------------------------------------------------------- |
99 | -- Info dictionary | 262 | -- Info dictionary |
100 | -----------------------------------------------------------------------} | 263 | -----------------------------------------------------------------------} |
@@ -145,9 +308,9 @@ instance Default InfoDict where | |||
145 | infoDictionary :: LayoutInfo -> PieceInfo -> Bool -> InfoDict | 308 | infoDictionary :: LayoutInfo -> PieceInfo -> Bool -> InfoDict |
146 | infoDictionary li pinfo private = InfoDict ih li pinfo private | 309 | infoDictionary li pinfo private = InfoDict ih li pinfo private |
147 | where | 310 | where |
148 | ih = hashLazyIH $ encode $ InfoDict def li pinfo private | 311 | ih = hashLazyIH $ BE.encode $ InfoDict def li pinfo private |
149 | 312 | ||
150 | getPrivate :: Get Bool | 313 | getPrivate :: BE.Get Bool |
151 | getPrivate = (Just True ==) <$>? "private" | 314 | getPrivate = (Just True ==) <$>? "private" |
152 | 315 | ||
153 | putPrivate :: Bool -> BDict -> BDict | 316 | putPrivate :: Bool -> BDict -> BDict |
@@ -172,7 +335,7 @@ instance BEncode InfoDict where | |||
172 | <*> getPieceInfo | 335 | <*> getPieceInfo |
173 | <*> getPrivate | 336 | <*> getPrivate |
174 | where | 337 | where |
175 | ih = hashLazyIH (encode dict) | 338 | ih = hashLazyIH (BE.encode dict) |
176 | 339 | ||
177 | ppPrivacy :: Bool -> Doc | 340 | ppPrivacy :: Bool -> Doc |
178 | ppPrivacy privacy = "Privacy: " <> if privacy then "private" else "public" | 341 | ppPrivacy privacy = "Privacy: " <> if privacy then "private" else "public" |
@@ -361,10 +524,314 @@ isTorrentPath filepath = takeExtension filepath == extSeparator : torrentExt | |||
361 | fromFile :: FilePath -> IO Torrent | 524 | fromFile :: FilePath -> IO Torrent |
362 | fromFile filepath = do | 525 | fromFile filepath = do |
363 | contents <- BS.readFile filepath | 526 | contents <- BS.readFile filepath |
364 | case decode contents of | 527 | case BE.decode contents of |
365 | Right !t -> return t | 528 | Right !t -> return t |
366 | Left msg -> throwIO $ userError $ msg ++ " while reading torrent file" | 529 | Left msg -> throwIO $ userError $ msg ++ " while reading torrent file" |
367 | 530 | ||
368 | -- | Encode and write a .torrent file. | 531 | -- | Encode and write a .torrent file. |
369 | toFile :: FilePath -> Torrent -> IO () | 532 | toFile :: FilePath -> Torrent -> IO () |
370 | toFile filepath = BL.writeFile filepath . encode | 533 | toFile filepath = BL.writeFile filepath . BE.encode |
534 | |||
535 | {----------------------------------------------------------------------- | ||
536 | -- URN | ||
537 | -----------------------------------------------------------------------} | ||
538 | |||
539 | -- | Namespace identifier determines the syntactic interpretation of | ||
540 | -- namespace-specific string. | ||
541 | type NamespaceId = [Text] | ||
542 | |||
543 | -- | BitTorrent Info Hash (hence the name) namespace | ||
544 | -- identifier. Namespace-specific string /should/ be a base16\/base32 | ||
545 | -- encoded SHA1 hash of the corresponding torrent /info/ dictionary. | ||
546 | -- | ||
547 | btih :: NamespaceId | ||
548 | btih = ["btih"] | ||
549 | |||
550 | -- | URN is pesistent location-independent identifier for | ||
551 | -- resources. In particular, URNs are used represent torrent names | ||
552 | -- as a part of magnet link, see 'Data.Torrent.Magnet.Magnet' for | ||
553 | -- more info. | ||
554 | -- | ||
555 | data URN = URN | ||
556 | { urnNamespace :: NamespaceId -- ^ a namespace identifier; | ||
557 | , urnString :: Text -- ^ a corresponding | ||
558 | -- namespace-specific string. | ||
559 | } deriving (Eq, Ord, Typeable) | ||
560 | |||
561 | ----------------------------------------------------------------------- | ||
562 | |||
563 | instance Convertible URN InfoHash where | ||
564 | safeConvert u @ URN {..} | ||
565 | | urnNamespace /= btih = convError "invalid namespace" u | ||
566 | | otherwise = safeConvert urnString | ||
567 | |||
568 | -- | Make resource name for torrent with corresponding | ||
569 | -- infohash. Infohash is base16 (hex) encoded. | ||
570 | -- | ||
571 | infohashURN :: InfoHash -> URN | ||
572 | infohashURN = URN btih . longHex | ||
573 | |||
574 | -- | Meaningless placeholder value. | ||
575 | instance Default URN where | ||
576 | def = infohashURN def | ||
577 | |||
578 | ------------------------------------------------------------------------ | ||
579 | |||
580 | -- | Render URN to its text representation. | ||
581 | renderURN :: URN -> Text | ||
582 | renderURN URN {..} | ||
583 | = T.intercalate ":" $ "urn" : urnNamespace ++ [urnString] | ||
584 | |||
585 | instance Pretty URN where | ||
586 | pretty = text . T.unpack . renderURN | ||
587 | |||
588 | instance Show URN where | ||
589 | showsPrec n = showsPrec n . T.unpack . renderURN | ||
590 | |||
591 | instance QueryValueLike URN where | ||
592 | toQueryValue = toQueryValue . renderURN | ||
593 | {-# INLINE toQueryValue #-} | ||
594 | |||
595 | ----------------------------------------------------------------------- | ||
596 | |||
597 | unsnoc :: [a] -> Maybe ([a], a) | ||
598 | unsnoc [] = Nothing | ||
599 | unsnoc xs = Just (L.init xs, L.last xs) | ||
600 | |||
601 | instance Convertible Text URN where | ||
602 | safeConvert t = case T.split (== ':') t of | ||
603 | uriScheme : body | ||
604 | | T.toLower uriScheme == "urn" -> | ||
605 | case unsnoc body of | ||
606 | Just (namespace, val) -> pure URN | ||
607 | { urnNamespace = namespace | ||
608 | , urnString = val | ||
609 | } | ||
610 | Nothing -> convError "missing URN string" body | ||
611 | | otherwise -> convError "invalid URN scheme" uriScheme | ||
612 | [] -> convError "missing URN scheme" t | ||
613 | |||
614 | instance IsString URN where | ||
615 | fromString = either (error . prettyConvertError) id | ||
616 | . safeConvert . T.pack | ||
617 | |||
618 | -- | Try to parse an URN from its text representation. | ||
619 | -- | ||
620 | -- Use 'safeConvert' for detailed error messages. | ||
621 | -- | ||
622 | parseURN :: Text -> Maybe URN | ||
623 | parseURN = either (const Nothing) pure . safeConvert | ||
624 | |||
625 | {----------------------------------------------------------------------- | ||
626 | -- Magnet | ||
627 | -----------------------------------------------------------------------} | ||
628 | -- $magnet-link | ||
629 | -- | ||
630 | -- Magnet URI scheme is an standard defining Magnet links. Magnet | ||
631 | -- links are refer to resources by hash, in particular magnet links | ||
632 | -- can refer to torrent using corresponding infohash. In this way, | ||
633 | -- magnet links can be used instead of torrent files. | ||
634 | -- | ||
635 | -- This module provides bittorrent specific implementation of magnet | ||
636 | -- links. | ||
637 | -- | ||
638 | -- For generic magnet uri scheme see: | ||
639 | -- <http://magnet-uri.sourceforge.net/magnet-draft-overview.txt>, | ||
640 | -- <http://www.iana.org/assignments/uri-schemes/prov/magnet> | ||
641 | -- | ||
642 | -- Bittorrent specific details: | ||
643 | -- <http://www.bittorrent.org/beps/bep_0009.html> | ||
644 | -- | ||
645 | |||
646 | -- TODO multiple exact topics | ||
647 | -- TODO render/parse supplement for URI/query | ||
648 | |||
649 | -- | An URI used to identify torrent. | ||
650 | data Magnet = Magnet | ||
651 | { -- | Torrent infohash hash. Can be used in DHT queries if no | ||
652 | -- 'tracker' provided. | ||
653 | exactTopic :: !InfoHash -- TODO InfoHash -> URN? | ||
654 | |||
655 | -- | A filename for the file to download. Can be used to | ||
656 | -- display name while waiting for metadata. | ||
657 | , displayName :: Maybe Text | ||
658 | |||
659 | -- | Size of the resource in bytes. | ||
660 | , exactLength :: Maybe Integer | ||
661 | |||
662 | -- | URI pointing to manifest, e.g. a list of further items. | ||
663 | , manifest :: Maybe Text | ||
664 | |||
665 | -- | Search string. | ||
666 | , keywordTopic :: Maybe Text | ||
667 | |||
668 | -- | A source to be queried after not being able to find and | ||
669 | -- download the file in the bittorrent network in a defined | ||
670 | -- amount of time. | ||
671 | , acceptableSource :: Maybe URI | ||
672 | |||
673 | -- | Direct link to the resource. | ||
674 | , exactSource :: Maybe URI | ||
675 | |||
676 | -- | URI to the tracker. | ||
677 | , tracker :: Maybe URI | ||
678 | |||
679 | -- | Additional or experimental parameters. | ||
680 | , supplement :: Map Text Text | ||
681 | } deriving (Eq, Ord, Typeable) | ||
682 | |||
683 | instance QueryValueLike Integer where | ||
684 | toQueryValue = toQueryValue . show | ||
685 | |||
686 | instance QueryValueLike URI where | ||
687 | toQueryValue = toQueryValue . show | ||
688 | |||
689 | instance QueryLike Magnet where | ||
690 | toQuery Magnet {..} = | ||
691 | [ ("xt", toQueryValue $ infohashURN exactTopic) | ||
692 | , ("dn", toQueryValue displayName) | ||
693 | , ("xl", toQueryValue exactLength) | ||
694 | , ("mt", toQueryValue manifest) | ||
695 | , ("kt", toQueryValue keywordTopic) | ||
696 | , ("as", toQueryValue acceptableSource) | ||
697 | , ("xs", toQueryValue exactSource) | ||
698 | , ("tr", toQueryValue tracker) | ||
699 | ] | ||
700 | |||
701 | instance QueryValueLike Magnet where | ||
702 | toQueryValue = toQueryValue . renderMagnet | ||
703 | |||
704 | instance Convertible QueryText Magnet where | ||
705 | safeConvert xs = do | ||
706 | urnStr <- getTextMsg "xt" "exact topic not defined" xs | ||
707 | infoHash <- convertVia (error "safeConvert" :: URN) urnStr | ||
708 | return Magnet | ||
709 | { exactTopic = infoHash | ||
710 | , displayName = getText "dn" xs | ||
711 | , exactLength = getText "xl" xs >>= getInt | ||
712 | , manifest = getText "mt" xs | ||
713 | , keywordTopic = getText "kt" xs | ||
714 | , acceptableSource = getText "as" xs >>= getURI | ||
715 | , exactSource = getText "xs" xs >>= getURI | ||
716 | , tracker = getText "tr" xs >>= getURI | ||
717 | , supplement = M.empty | ||
718 | } | ||
719 | where | ||
720 | getInt = either (const Nothing) (Just . fst) . signed decimal | ||
721 | getURI = parseURI . T.unpack | ||
722 | getText p = join . L.lookup p | ||
723 | getTextMsg p msg ps = maybe (convError msg xs) pure $ getText p ps | ||
724 | |||
725 | magnetScheme :: URI | ||
726 | magnetScheme = URI | ||
727 | { uriScheme = "magnet:" | ||
728 | , uriAuthority = Nothing | ||
729 | , uriPath = "" | ||
730 | , uriQuery = "" | ||
731 | , uriFragment = "" | ||
732 | } | ||
733 | |||
734 | isMagnetURI :: URI -> Bool | ||
735 | isMagnetURI u = u { uriQuery = "" } == magnetScheme | ||
736 | |||
737 | -- | Can be used instead of 'parseMagnet'. | ||
738 | instance Convertible URI Magnet where | ||
739 | safeConvert u @ URI {..} | ||
740 | | not (isMagnetURI u) = convError "this is not a magnet link" u | ||
741 | | otherwise = safeConvert $ parseQueryText $ BC.pack uriQuery | ||
742 | |||
743 | -- | Can be used instead of 'renderMagnet'. | ||
744 | instance Convertible Magnet URI where | ||
745 | safeConvert m = pure $ magnetScheme | ||
746 | { uriQuery = BC.unpack $ renderQuery True $ toQuery m } | ||
747 | |||
748 | instance Convertible String Magnet where | ||
749 | safeConvert str | ||
750 | | Just uri <- parseURI str = safeConvert uri | ||
751 | | otherwise = convError "unable to parse uri" str | ||
752 | |||
753 | ------------------------------------------------------------------------ | ||
754 | |||
755 | -- | Meaningless placeholder value. | ||
756 | instance Default Magnet where | ||
757 | def = Magnet | ||
758 | { exactTopic = def | ||
759 | , displayName = Nothing | ||
760 | , exactLength = Nothing | ||
761 | , manifest = Nothing | ||
762 | , keywordTopic = Nothing | ||
763 | , acceptableSource = Nothing | ||
764 | , exactSource = Nothing | ||
765 | , tracker = Nothing | ||
766 | , supplement = M.empty | ||
767 | } | ||
768 | |||
769 | -- | Set 'exactTopic' ('xt' param) only, other params are empty. | ||
770 | nullMagnet :: InfoHash -> Magnet | ||
771 | nullMagnet u = Magnet | ||
772 | { exactTopic = u | ||
773 | , displayName = Nothing | ||
774 | , exactLength = Nothing | ||
775 | , manifest = Nothing | ||
776 | , keywordTopic = Nothing | ||
777 | , acceptableSource = Nothing | ||
778 | , exactSource = Nothing | ||
779 | , tracker = Nothing | ||
780 | , supplement = M.empty | ||
781 | } | ||
782 | |||
783 | -- | Like 'nullMagnet' but also include 'displayName' ('dn' param). | ||
784 | simpleMagnet :: Torrent -> Magnet | ||
785 | simpleMagnet Torrent {tInfoDict = InfoDict {..}} | ||
786 | = (nullMagnet idInfoHash) | ||
787 | { displayName = Just $ T.decodeUtf8 $ suggestedName idLayoutInfo | ||
788 | } | ||
789 | |||
790 | -- | Like 'simpleMagnet' but also include 'exactLength' ('xl' param) and | ||
791 | -- 'tracker' ('tr' param). | ||
792 | -- | ||
793 | detailedMagnet :: Torrent -> Magnet | ||
794 | detailedMagnet t @ Torrent {tInfoDict = InfoDict {..}, tAnnounce} | ||
795 | = (simpleMagnet t) | ||
796 | { exactLength = Just $ fromIntegral $ contentLength idLayoutInfo | ||
797 | , tracker = tAnnounce | ||
798 | } | ||
799 | |||
800 | ----------------------------------------------------------------------- | ||
801 | |||
802 | parseMagnetStr :: String -> Maybe Magnet | ||
803 | parseMagnetStr = either (const Nothing) Just . safeConvert | ||
804 | |||
805 | renderMagnetStr :: Magnet -> String | ||
806 | renderMagnetStr = show . (convert :: Magnet -> URI) | ||
807 | |||
808 | instance Pretty Magnet where | ||
809 | pretty = PP.text . renderMagnetStr | ||
810 | |||
811 | instance Show Magnet where | ||
812 | show = renderMagnetStr | ||
813 | {-# INLINE show #-} | ||
814 | |||
815 | instance Read Magnet where | ||
816 | readsPrec _ xs | ||
817 | | Just m <- parseMagnetStr mstr = [(m, rest)] | ||
818 | | otherwise = [] | ||
819 | where | ||
820 | (mstr, rest) = L.break (== ' ') xs | ||
821 | |||
822 | instance IsString Magnet where | ||
823 | fromString str = fromMaybe (error msg) $ parseMagnetStr str | ||
824 | where | ||
825 | msg = "unable to parse magnet: " ++ str | ||
826 | |||
827 | -- | Try to parse magnet link from urlencoded string. Use | ||
828 | -- 'safeConvert' to find out error location. | ||
829 | -- | ||
830 | parseMagnet :: Text -> Maybe Magnet | ||
831 | parseMagnet = parseMagnetStr . T.unpack | ||
832 | {-# INLINE parseMagnet #-} | ||
833 | |||
834 | -- | Render magnet link to urlencoded string | ||
835 | renderMagnet :: Magnet -> Text | ||
836 | renderMagnet = T.pack . renderMagnetStr | ||
837 | {-# INLINE renderMagnet #-} | ||
diff --git a/src/Data/Torrent/InfoHash.hs b/src/Data/Torrent/InfoHash.hs deleted file mode 100644 index f322ac6f..00000000 --- a/src/Data/Torrent/InfoHash.hs +++ /dev/null | |||
@@ -1,164 +0,0 @@ | |||
1 | -- | | ||
2 | -- Copyright : (c) Sam Truzjan 2013 | ||
3 | -- License : BSD3 | ||
4 | -- Maintainer : pxqr.sta@gmail.com | ||
5 | -- Stability : provisional | ||
6 | -- Portability : portable | ||
7 | -- | ||
8 | -- Infohash is a unique identifier of torrent. | ||
9 | -- | ||
10 | {-# LANGUAGE FlexibleInstances #-} | ||
11 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} | ||
12 | {-# LANGUAGE MultiParamTypeClasses #-} | ||
13 | {-# LANGUAGE DeriveDataTypeable #-} | ||
14 | module Data.Torrent.InfoHash | ||
15 | ( InfoHash | ||
16 | |||
17 | -- * Parsing | ||
18 | , textToInfoHash | ||
19 | |||
20 | -- * Rendering | ||
21 | , longHex | ||
22 | , shortHex | ||
23 | ) where | ||
24 | |||
25 | import Control.Applicative | ||
26 | import Control.Monad | ||
27 | import Data.BEncode | ||
28 | import Data.ByteString as BS | ||
29 | import Data.ByteString.Char8 as BC | ||
30 | import Data.ByteString.Base16 as Base16 | ||
31 | import Data.ByteString.Base32 as Base32 | ||
32 | import Data.ByteString.Base64 as Base64 | ||
33 | import Data.Char | ||
34 | import Data.Convertible.Base | ||
35 | import Data.Default | ||
36 | import Data.List as L | ||
37 | import Data.Hashable as Hashable | ||
38 | import Data.Serialize | ||
39 | import Data.String | ||
40 | import Data.Text as T | ||
41 | import Data.Text.Encoding as T | ||
42 | import Data.Typeable | ||
43 | import Network.HTTP.Types.QueryLike | ||
44 | import Text.ParserCombinators.ReadP as P | ||
45 | import Text.PrettyPrint | ||
46 | import Text.PrettyPrint.Class | ||
47 | |||
48 | |||
49 | -- TODO | ||
50 | -- | ||
51 | -- data Word160 = Word160 {-# UNPACK #-} !Word64 | ||
52 | -- {-# UNPACK #-} !Word64 | ||
53 | -- {-# UNPACK #-} !Word32 | ||
54 | -- | ||
55 | -- newtype InfoHash = InfoHash Word160 | ||
56 | -- | ||
57 | -- reason: bytestring have overhead = 8 words, while infohash have length 20 bytes | ||
58 | |||
59 | -- | Exactly 20 bytes long SHA1 hash of the info part of torrent file. | ||
60 | newtype InfoHash = InfoHash { getInfoHash :: BS.ByteString } | ||
61 | deriving (Eq, Ord, Typeable) | ||
62 | |||
63 | infoHashLen :: Int | ||
64 | infoHashLen = 20 | ||
65 | |||
66 | -- | Meaningless placeholder value. | ||
67 | instance Default InfoHash where | ||
68 | def = "0123456789012345678901234567890123456789" | ||
69 | |||
70 | -- | Hash raw bytes. (no encoding) | ||
71 | instance Hashable InfoHash where | ||
72 | hashWithSalt s (InfoHash ih) = hashWithSalt s ih | ||
73 | {-# INLINE hashWithSalt #-} | ||
74 | |||
75 | -- | Convert to\/from raw bencoded string. (no encoding) | ||
76 | instance BEncode InfoHash where | ||
77 | toBEncode = toBEncode . getInfoHash | ||
78 | fromBEncode be = InfoHash <$> fromBEncode be | ||
79 | |||
80 | -- | Convert to\/from raw bytestring. (no encoding) | ||
81 | instance Serialize InfoHash where | ||
82 | put (InfoHash ih) = putByteString ih | ||
83 | {-# INLINE put #-} | ||
84 | |||
85 | get = InfoHash <$> getBytes infoHashLen | ||
86 | {-# INLINE get #-} | ||
87 | |||
88 | -- | Convert to raw query value. (no encoding) | ||
89 | instance QueryValueLike InfoHash where | ||
90 | toQueryValue (InfoHash ih) = Just ih | ||
91 | {-# INLINE toQueryValue #-} | ||
92 | |||
93 | -- | Convert to base16 encoded string. | ||
94 | instance Show InfoHash where | ||
95 | show (InfoHash ih) = BC.unpack (Base16.encode ih) | ||
96 | |||
97 | -- | Convert to base16 encoded Doc string. | ||
98 | instance Pretty InfoHash where | ||
99 | pretty = text . show | ||
100 | |||
101 | -- | Read base16 encoded string. | ||
102 | instance Read InfoHash where | ||
103 | readsPrec _ = readP_to_S $ do | ||
104 | str <- replicateM (infoHashLen * 2) (satisfy isHexDigit) | ||
105 | return $ InfoHash $ decodeIH str | ||
106 | where | ||
107 | decodeIH = BS.pack . L.map fromHex . pair | ||
108 | fromHex (a, b) = read $ '0' : 'x' : a : b : [] | ||
109 | |||
110 | pair (a : b : xs) = (a, b) : pair xs | ||
111 | pair _ = [] | ||
112 | |||
113 | -- | Convert raw bytes to info hash. | ||
114 | instance Convertible BS.ByteString InfoHash where | ||
115 | safeConvert bs | ||
116 | | BS.length bs == infoHashLen = pure (InfoHash bs) | ||
117 | | otherwise = convError "invalid length" bs | ||
118 | |||
119 | -- | Parse infohash from base16\/base32\/base64 encoded string. | ||
120 | instance Convertible Text InfoHash where | ||
121 | safeConvert t | ||
122 | | 20 == hashLen = pure (InfoHash hashStr) | ||
123 | | 26 <= hashLen && hashLen <= 28 = | ||
124 | case Base64.decode hashStr of | ||
125 | Left msg -> convError ("invalid base64 encoding " ++ msg) t | ||
126 | Right ihStr -> safeConvert ihStr | ||
127 | |||
128 | | hashLen == 32 = | ||
129 | case Base32.decode hashStr of | ||
130 | Left msg -> convError msg t | ||
131 | Right ihStr -> safeConvert ihStr | ||
132 | |||
133 | | hashLen == 40 = | ||
134 | let (ihStr, inv) = Base16.decode hashStr | ||
135 | in if BS.length inv /= 0 | ||
136 | then convError "invalid base16 encoding" t | ||
137 | else safeConvert ihStr | ||
138 | |||
139 | | otherwise = convError "invalid length" t | ||
140 | where | ||
141 | hashLen = BS.length hashStr | ||
142 | hashStr = T.encodeUtf8 t | ||
143 | |||
144 | -- | Decode from base16\/base32\/base64 encoded string. | ||
145 | instance IsString InfoHash where | ||
146 | fromString = either (error . prettyConvertError) id . safeConvert . T.pack | ||
147 | |||
148 | ignoreErrorMsg :: Either a b -> Maybe b | ||
149 | ignoreErrorMsg = either (const Nothing) Just | ||
150 | |||
151 | -- | Tries both base16 and base32 while decoding info hash. | ||
152 | -- | ||
153 | -- Use 'safeConvert' for detailed error messages. | ||
154 | -- | ||
155 | textToInfoHash :: Text -> Maybe InfoHash | ||
156 | textToInfoHash = ignoreErrorMsg . safeConvert | ||
157 | |||
158 | -- | Hex encode infohash to text, full length. | ||
159 | longHex :: InfoHash -> Text | ||
160 | longHex = T.decodeUtf8 . Base16.encode . getInfoHash | ||
161 | |||
162 | -- | The same as 'longHex', but only first 7 characters. | ||
163 | shortHex :: InfoHash -> Text | ||
164 | shortHex = T.take 7 . longHex | ||
diff --git a/src/Data/Torrent/Magnet.hs b/src/Data/Torrent/Magnet.hs deleted file mode 100644 index aad0debe..00000000 --- a/src/Data/Torrent/Magnet.hs +++ /dev/null | |||
@@ -1,372 +0,0 @@ | |||
1 | -- | | ||
2 | -- Copyright : (c) Sam Truzjan 2013 | ||
3 | -- License : BSD3 | ||
4 | -- Maintainer : pxqr.sta@gmail.com | ||
5 | -- Stability : provisional | ||
6 | -- Portability : portable | ||
7 | -- | ||
8 | -- Magnet URI scheme is an standard defining Magnet links. Magnet | ||
9 | -- links are refer to resources by hash, in particular magnet links | ||
10 | -- can refer to torrent using corresponding infohash. In this way, | ||
11 | -- magnet links can be used instead of torrent files. | ||
12 | -- | ||
13 | -- This module provides bittorrent specific implementation of magnet | ||
14 | -- links. | ||
15 | -- | ||
16 | -- For generic magnet uri scheme see: | ||
17 | -- <http://magnet-uri.sourceforge.net/magnet-draft-overview.txt>, | ||
18 | -- <http://www.iana.org/assignments/uri-schemes/prov/magnet> | ||
19 | -- | ||
20 | -- Bittorrent specific details: | ||
21 | -- <http://www.bittorrent.org/beps/bep_0009.html> | ||
22 | -- | ||
23 | {-# LANGUAGE NamedFieldPuns #-} | ||
24 | {-# LANGUAGE FlexibleInstances #-} | ||
25 | {-# LANGUAGE MultiParamTypeClasses #-} | ||
26 | {-# LANGUAGE TypeSynonymInstances #-} | ||
27 | {-# LANGUAGE DeriveDataTypeable #-} | ||
28 | {-# OPTIONS -fno-warn-orphans #-} | ||
29 | module Data.Torrent.Magnet | ||
30 | ( -- * Magnet | ||
31 | Magnet(..) | ||
32 | |||
33 | -- ** Construction | ||
34 | , nullMagnet | ||
35 | , simpleMagnet | ||
36 | , detailedMagnet | ||
37 | |||
38 | -- ** Conversion | ||
39 | , parseMagnet | ||
40 | , renderMagnet | ||
41 | |||
42 | -- * URN | ||
43 | , URN (..) | ||
44 | |||
45 | -- ** Namespaces | ||
46 | , NamespaceId | ||
47 | , btih | ||
48 | |||
49 | -- ** Construction | ||
50 | , infohashURN | ||
51 | |||
52 | -- ** Conversion | ||
53 | , parseURN | ||
54 | , renderURN | ||
55 | ) where | ||
56 | |||
57 | import Control.Applicative | ||
58 | import Control.Monad | ||
59 | import Data.ByteString.Char8 as BC | ||
60 | import Data.Convertible | ||
61 | import Data.Default | ||
62 | import Data.Map as M | ||
63 | import Data.Maybe | ||
64 | import Data.List as L | ||
65 | import Data.String | ||
66 | import Data.Text as T | ||
67 | import Data.Text.Encoding as T | ||
68 | import Data.Text.Read | ||
69 | import Data.Typeable | ||
70 | import Network.HTTP.Types.QueryLike | ||
71 | import Network.HTTP.Types.URI | ||
72 | import Network.URI | ||
73 | import Text.PrettyPrint as PP | ||
74 | import Text.PrettyPrint.Class | ||
75 | |||
76 | import Data.Torrent | ||
77 | import Data.Torrent.InfoHash | ||
78 | import Data.Torrent.Layout | ||
79 | |||
80 | |||
81 | -- | Namespace identifier determines the syntactic interpretation of | ||
82 | -- namespace-specific string. | ||
83 | type NamespaceId = [Text] | ||
84 | |||
85 | -- | BitTorrent Info Hash (hence the name) namespace | ||
86 | -- identifier. Namespace-specific string /should/ be a base16\/base32 | ||
87 | -- encoded SHA1 hash of the corresponding torrent /info/ dictionary. | ||
88 | -- | ||
89 | btih :: NamespaceId | ||
90 | btih = ["btih"] | ||
91 | |||
92 | -- | URN is pesistent location-independent identifier for | ||
93 | -- resources. In particular, URNs are used represent torrent names | ||
94 | -- as a part of magnet link, see 'Data.Torrent.Magnet.Magnet' for | ||
95 | -- more info. | ||
96 | -- | ||
97 | data URN = URN | ||
98 | { urnNamespace :: NamespaceId -- ^ a namespace identifier; | ||
99 | , urnString :: Text -- ^ a corresponding | ||
100 | -- namespace-specific string. | ||
101 | } deriving (Eq, Ord, Typeable) | ||
102 | |||
103 | {----------------------------------------------------------------------- | ||
104 | -- URN to infohash convertion | ||
105 | -----------------------------------------------------------------------} | ||
106 | |||
107 | instance Convertible URN InfoHash where | ||
108 | safeConvert u @ URN {..} | ||
109 | | urnNamespace /= btih = convError "invalid namespace" u | ||
110 | | otherwise = safeConvert urnString | ||
111 | |||
112 | -- | Make resource name for torrent with corresponding | ||
113 | -- infohash. Infohash is base16 (hex) encoded. | ||
114 | -- | ||
115 | infohashURN :: InfoHash -> URN | ||
116 | infohashURN = URN btih . longHex | ||
117 | |||
118 | -- | Meaningless placeholder value. | ||
119 | instance Default URN where | ||
120 | def = infohashURN def | ||
121 | |||
122 | {----------------------------------------------------------------------- | ||
123 | -- URN Rendering | ||
124 | -----------------------------------------------------------------------} | ||
125 | |||
126 | -- | Render URN to its text representation. | ||
127 | renderURN :: URN -> Text | ||
128 | renderURN URN {..} | ||
129 | = T.intercalate ":" $ "urn" : urnNamespace ++ [urnString] | ||
130 | |||
131 | instance Pretty URN where | ||
132 | pretty = text . T.unpack . renderURN | ||
133 | |||
134 | instance Show URN where | ||
135 | showsPrec n = showsPrec n . T.unpack . renderURN | ||
136 | |||
137 | instance QueryValueLike URN where | ||
138 | toQueryValue = toQueryValue . renderURN | ||
139 | {-# INLINE toQueryValue #-} | ||
140 | |||
141 | {----------------------------------------------------------------------- | ||
142 | -- URN Parsing | ||
143 | -----------------------------------------------------------------------} | ||
144 | |||
145 | unsnoc :: [a] -> Maybe ([a], a) | ||
146 | unsnoc [] = Nothing | ||
147 | unsnoc xs = Just (L.init xs, L.last xs) | ||
148 | |||
149 | instance Convertible Text URN where | ||
150 | safeConvert t = case T.split (== ':') t of | ||
151 | uriScheme : body | ||
152 | | T.toLower uriScheme == "urn" -> | ||
153 | case unsnoc body of | ||
154 | Just (namespace, val) -> pure URN | ||
155 | { urnNamespace = namespace | ||
156 | , urnString = val | ||
157 | } | ||
158 | Nothing -> convError "missing URN string" body | ||
159 | | otherwise -> convError "invalid URN scheme" uriScheme | ||
160 | [] -> convError "missing URN scheme" t | ||
161 | |||
162 | instance IsString URN where | ||
163 | fromString = either (error . prettyConvertError) id | ||
164 | . safeConvert . T.pack | ||
165 | |||
166 | -- | Try to parse an URN from its text representation. | ||
167 | -- | ||
168 | -- Use 'safeConvert' for detailed error messages. | ||
169 | -- | ||
170 | parseURN :: Text -> Maybe URN | ||
171 | parseURN = either (const Nothing) pure . safeConvert | ||
172 | |||
173 | {----------------------------------------------------------------------- | ||
174 | -- Magnet | ||
175 | -----------------------------------------------------------------------} | ||
176 | |||
177 | -- TODO multiple exact topics | ||
178 | -- TODO render/parse supplement for URI/query | ||
179 | |||
180 | -- | An URI used to identify torrent. | ||
181 | data Magnet = Magnet | ||
182 | { -- | Torrent infohash hash. Can be used in DHT queries if no | ||
183 | -- 'tracker' provided. | ||
184 | exactTopic :: !InfoHash -- TODO InfoHash -> URN? | ||
185 | |||
186 | -- | A filename for the file to download. Can be used to | ||
187 | -- display name while waiting for metadata. | ||
188 | , displayName :: Maybe Text | ||
189 | |||
190 | -- | Size of the resource in bytes. | ||
191 | , exactLength :: Maybe Integer | ||
192 | |||
193 | -- | URI pointing to manifest, e.g. a list of further items. | ||
194 | , manifest :: Maybe Text | ||
195 | |||
196 | -- | Search string. | ||
197 | , keywordTopic :: Maybe Text | ||
198 | |||
199 | -- | A source to be queried after not being able to find and | ||
200 | -- download the file in the bittorrent network in a defined | ||
201 | -- amount of time. | ||
202 | , acceptableSource :: Maybe URI | ||
203 | |||
204 | -- | Direct link to the resource. | ||
205 | , exactSource :: Maybe URI | ||
206 | |||
207 | -- | URI to the tracker. | ||
208 | , tracker :: Maybe URI | ||
209 | |||
210 | -- | Additional or experimental parameters. | ||
211 | , supplement :: Map Text Text | ||
212 | } deriving (Eq, Ord, Typeable) | ||
213 | |||
214 | instance QueryValueLike Integer where | ||
215 | toQueryValue = toQueryValue . show | ||
216 | |||
217 | instance QueryValueLike URI where | ||
218 | toQueryValue = toQueryValue . show | ||
219 | |||
220 | instance QueryLike Magnet where | ||
221 | toQuery Magnet {..} = | ||
222 | [ ("xt", toQueryValue $ infohashURN exactTopic) | ||
223 | , ("dn", toQueryValue displayName) | ||
224 | , ("xl", toQueryValue exactLength) | ||
225 | , ("mt", toQueryValue manifest) | ||
226 | , ("kt", toQueryValue keywordTopic) | ||
227 | , ("as", toQueryValue acceptableSource) | ||
228 | , ("xs", toQueryValue exactSource) | ||
229 | , ("tr", toQueryValue tracker) | ||
230 | ] | ||
231 | |||
232 | instance QueryValueLike Magnet where | ||
233 | toQueryValue = toQueryValue . renderMagnet | ||
234 | |||
235 | instance Convertible QueryText Magnet where | ||
236 | safeConvert xs = do | ||
237 | urnStr <- getTextMsg "xt" "exact topic not defined" xs | ||
238 | infoHash <- convertVia (error "safeConvert" :: URN) urnStr | ||
239 | return Magnet | ||
240 | { exactTopic = infoHash | ||
241 | , displayName = getText "dn" xs | ||
242 | , exactLength = getText "xl" xs >>= getInt | ||
243 | , manifest = getText "mt" xs | ||
244 | , keywordTopic = getText "kt" xs | ||
245 | , acceptableSource = getText "as" xs >>= getURI | ||
246 | , exactSource = getText "xs" xs >>= getURI | ||
247 | , tracker = getText "tr" xs >>= getURI | ||
248 | , supplement = M.empty | ||
249 | } | ||
250 | where | ||
251 | getInt = either (const Nothing) (Just . fst) . signed decimal | ||
252 | getURI = parseURI . T.unpack | ||
253 | getText p = join . L.lookup p | ||
254 | getTextMsg p msg ps = maybe (convError msg xs) pure $ getText p ps | ||
255 | |||
256 | magnetScheme :: URI | ||
257 | magnetScheme = URI | ||
258 | { uriScheme = "magnet:" | ||
259 | , uriAuthority = Nothing | ||
260 | , uriPath = "" | ||
261 | , uriQuery = "" | ||
262 | , uriFragment = "" | ||
263 | } | ||
264 | |||
265 | isMagnetURI :: URI -> Bool | ||
266 | isMagnetURI u = u { uriQuery = "" } == magnetScheme | ||
267 | |||
268 | -- | Can be used instead of 'parseMagnet'. | ||
269 | instance Convertible URI Magnet where | ||
270 | safeConvert u @ URI {..} | ||
271 | | not (isMagnetURI u) = convError "this is not a magnet link" u | ||
272 | | otherwise = safeConvert $ parseQueryText $ BC.pack uriQuery | ||
273 | |||
274 | -- | Can be used instead of 'renderMagnet'. | ||
275 | instance Convertible Magnet URI where | ||
276 | safeConvert m = pure $ magnetScheme | ||
277 | { uriQuery = BC.unpack $ renderQuery True $ toQuery m } | ||
278 | |||
279 | instance Convertible String Magnet where | ||
280 | safeConvert str | ||
281 | | Just uri <- parseURI str = safeConvert uri | ||
282 | | otherwise = convError "unable to parse uri" str | ||
283 | |||
284 | {----------------------------------------------------------------------- | ||
285 | -- Magnet Construction | ||
286 | -----------------------------------------------------------------------} | ||
287 | |||
288 | -- | Meaningless placeholder value. | ||
289 | instance Default Magnet where | ||
290 | def = Magnet | ||
291 | { exactTopic = def | ||
292 | , displayName = Nothing | ||
293 | , exactLength = Nothing | ||
294 | , manifest = Nothing | ||
295 | , keywordTopic = Nothing | ||
296 | , acceptableSource = Nothing | ||
297 | , exactSource = Nothing | ||
298 | , tracker = Nothing | ||
299 | , supplement = M.empty | ||
300 | } | ||
301 | |||
302 | -- | Set 'exactTopic' ('xt' param) only, other params are empty. | ||
303 | nullMagnet :: InfoHash -> Magnet | ||
304 | nullMagnet u = Magnet | ||
305 | { exactTopic = u | ||
306 | , displayName = Nothing | ||
307 | , exactLength = Nothing | ||
308 | , manifest = Nothing | ||
309 | , keywordTopic = Nothing | ||
310 | , acceptableSource = Nothing | ||
311 | , exactSource = Nothing | ||
312 | , tracker = Nothing | ||
313 | , supplement = M.empty | ||
314 | } | ||
315 | |||
316 | -- | Like 'nullMagnet' but also include 'displayName' ('dn' param). | ||
317 | simpleMagnet :: Torrent -> Magnet | ||
318 | simpleMagnet Torrent {tInfoDict = InfoDict {..}} | ||
319 | = (nullMagnet idInfoHash) | ||
320 | { displayName = Just $ T.decodeUtf8 $ suggestedName idLayoutInfo | ||
321 | } | ||
322 | |||
323 | -- | Like 'simpleMagnet' but also include 'exactLength' ('xl' param) and | ||
324 | -- 'tracker' ('tr' param). | ||
325 | -- | ||
326 | detailedMagnet :: Torrent -> Magnet | ||
327 | detailedMagnet t @ Torrent {tInfoDict = InfoDict {..}, tAnnounce} | ||
328 | = (simpleMagnet t) | ||
329 | { exactLength = Just $ fromIntegral $ contentLength idLayoutInfo | ||
330 | , tracker = tAnnounce | ||
331 | } | ||
332 | |||
333 | {----------------------------------------------------------------------- | ||
334 | -- Magnet Conversion | ||
335 | -----------------------------------------------------------------------} | ||
336 | |||
337 | parseMagnetStr :: String -> Maybe Magnet | ||
338 | parseMagnetStr = either (const Nothing) Just . safeConvert | ||
339 | |||
340 | renderMagnetStr :: Magnet -> String | ||
341 | renderMagnetStr = show . (convert :: Magnet -> URI) | ||
342 | |||
343 | instance Pretty Magnet where | ||
344 | pretty = PP.text . renderMagnetStr | ||
345 | |||
346 | instance Show Magnet where | ||
347 | show = renderMagnetStr | ||
348 | {-# INLINE show #-} | ||
349 | |||
350 | instance Read Magnet where | ||
351 | readsPrec _ xs | ||
352 | | Just m <- parseMagnetStr mstr = [(m, rest)] | ||
353 | | otherwise = [] | ||
354 | where | ||
355 | (mstr, rest) = L.break (== ' ') xs | ||
356 | |||
357 | instance IsString Magnet where | ||
358 | fromString str = fromMaybe (error msg) $ parseMagnetStr str | ||
359 | where | ||
360 | msg = "unable to parse magnet: " ++ str | ||
361 | |||
362 | -- | Try to parse magnet link from urlencoded string. Use | ||
363 | -- 'safeConvert' to find out error location. | ||
364 | -- | ||
365 | parseMagnet :: Text -> Maybe Magnet | ||
366 | parseMagnet = parseMagnetStr . T.unpack | ||
367 | {-# INLINE parseMagnet #-} | ||
368 | |||
369 | -- | Render magnet link to urlencoded string | ||
370 | renderMagnet :: Magnet -> Text | ||
371 | renderMagnet = T.pack . renderMagnetStr | ||
372 | {-# INLINE renderMagnet #-} | ||