summaryrefslogtreecommitdiff
path: root/src/Data
diff options
context:
space:
mode:
authorSam Truzjan <pxqr.sta@gmail.com>2014-04-04 20:30:54 +0400
committerSam Truzjan <pxqr.sta@gmail.com>2014-04-04 20:30:54 +0400
commit7a892425de92efd88b98576e848bebc725a9bf14 (patch)
treeb872eaf1bfd6cfe5d302f31f9c9b7c1a5b6d0a61 /src/Data
parent9b2981d38cfa188099cca07337a3b63747e2c527 (diff)
Move Infohash and Magnet to Torrent module
Diffstat (limited to 'src/Data')
-rw-r--r--src/Data/Torrent.hs489
-rw-r--r--src/Data/Torrent/InfoHash.hs164
-rw-r--r--src/Data/Torrent/Magnet.hs372
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 #-}
27module Data.Torrent 29module 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
68import Prelude hiding (sum) 94import Prelude hiding (sum)
69import Control.Applicative 95import Control.Applicative
70import qualified Crypto.Hash.SHA1 as C
71import Control.DeepSeq 96import Control.DeepSeq
72import Control.Exception 97import Control.Exception
73import Control.Lens 98import Control.Lens hiding (unsnoc)
99import Control.Monad
100import qualified Crypto.Hash.SHA1 as C
74import Data.BEncode as BE 101import Data.BEncode as BE
75import Data.BEncode.Types as BE 102import Data.BEncode.Types as BE
76import Data.ByteString as BS 103import Data.ByteString as BS
104import Data.ByteString.Base16 as Base16
105import Data.ByteString.Base32 as Base32
106import Data.ByteString.Base64 as Base64
77import qualified Data.ByteString.Char8 as BC (pack, unpack) 107import qualified Data.ByteString.Char8 as BC (pack, unpack)
78import qualified Data.ByteString.Lazy as BL 108import qualified Data.ByteString.Lazy as BL
109import Data.Char
79import Data.Convertible 110import Data.Convertible
80import Data.Default 111import Data.Default
81import Data.Hashable as Hashable 112import Data.Hashable as Hashable
82import qualified Data.List as L 113import qualified Data.List as L
114import Data.Map as M
115import Data.Maybe
116import Data.Serialize as S
117import Data.String
83import Data.Text as T 118import Data.Text as T
84import Data.Time 119import Data.Text.Encoding as T
120import Data.Text.Read
85import Data.Time.Clock.POSIX 121import Data.Time.Clock.POSIX
86import Data.Typeable 122import Data.Typeable
87import Network (HostName) 123import Network (HostName)
124import Network.HTTP.Types.QueryLike
125import Network.HTTP.Types.URI
88import Network.URI 126import Network.URI
127import Text.ParserCombinators.ReadP as P
89import Text.PrettyPrint as PP 128import Text.PrettyPrint as PP
90import Text.PrettyPrint.Class 129import Text.PrettyPrint.Class
91import System.FilePath 130import System.FilePath
92 131
93import Data.Torrent.InfoHash as IH
94import Data.Torrent.Layout 132import Data.Torrent.Layout
95import Data.Torrent.Piece 133import Data.Torrent.Piece
96import Network.BitTorrent.Core.NodeInfo 134import 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.
155newtype InfoHash = InfoHash { getInfoHash :: BS.ByteString }
156 deriving (Eq, Ord, Typeable)
157
158infoHashLen :: Int
159infoHashLen = 20
160
161-- | Meaningless placeholder value.
162instance Default InfoHash where
163 def = "0123456789012345678901234567890123456789"
164
165-- | Hash raw bytes. (no encoding)
166instance Hashable InfoHash where
167 hashWithSalt s (InfoHash ih) = hashWithSalt s ih
168 {-# INLINE hashWithSalt #-}
169
170-- | Convert to\/from raw bencoded string. (no encoding)
171instance BEncode InfoHash where
172 toBEncode = toBEncode . getInfoHash
173 fromBEncode be = InfoHash <$> fromBEncode be
174
175-- | Convert to\/from raw bytestring. (no encoding)
176instance 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)
184instance QueryValueLike InfoHash where
185 toQueryValue (InfoHash ih) = Just ih
186 {-# INLINE toQueryValue #-}
187
188-- | Convert to base16 encoded string.
189instance Show InfoHash where
190 show (InfoHash ih) = BC.unpack (Base16.encode ih)
191
192-- | Convert to base16 encoded Doc string.
193instance Pretty InfoHash where
194 pretty = text . show
195
196-- | Read base16 encoded string.
197instance 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.
209instance 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.
215instance 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.
240instance IsString InfoHash where
241 fromString = either (error . prettyConvertError) id . safeConvert . T.pack
242
243ignoreErrorMsg :: Either a b -> Maybe b
244ignoreErrorMsg = either (const Nothing) Just
245
246-- | Tries both base16 and base32 while decoding info hash.
247--
248-- Use 'safeConvert' for detailed error messages.
249--
250textToInfoHash :: Text -> Maybe InfoHash
251textToInfoHash = ignoreErrorMsg . safeConvert
252
253-- | Hex encode infohash to text, full length.
254longHex :: InfoHash -> Text
255longHex = T.decodeUtf8 . Base16.encode . getInfoHash
256
257-- | The same as 'longHex', but only first 7 characters.
258shortHex :: InfoHash -> Text
259shortHex = T.take 7 . longHex
260
98{----------------------------------------------------------------------- 261{-----------------------------------------------------------------------
99-- Info dictionary 262-- Info dictionary
100-----------------------------------------------------------------------} 263-----------------------------------------------------------------------}
@@ -145,9 +308,9 @@ instance Default InfoDict where
145infoDictionary :: LayoutInfo -> PieceInfo -> Bool -> InfoDict 308infoDictionary :: LayoutInfo -> PieceInfo -> Bool -> InfoDict
146infoDictionary li pinfo private = InfoDict ih li pinfo private 309infoDictionary 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
150getPrivate :: Get Bool 313getPrivate :: BE.Get Bool
151getPrivate = (Just True ==) <$>? "private" 314getPrivate = (Just True ==) <$>? "private"
152 315
153putPrivate :: Bool -> BDict -> BDict 316putPrivate :: 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
177ppPrivacy :: Bool -> Doc 340ppPrivacy :: Bool -> Doc
178ppPrivacy privacy = "Privacy: " <> if privacy then "private" else "public" 341ppPrivacy privacy = "Privacy: " <> if privacy then "private" else "public"
@@ -361,10 +524,314 @@ isTorrentPath filepath = takeExtension filepath == extSeparator : torrentExt
361fromFile :: FilePath -> IO Torrent 524fromFile :: FilePath -> IO Torrent
362fromFile filepath = do 525fromFile 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.
369toFile :: FilePath -> Torrent -> IO () 532toFile :: FilePath -> Torrent -> IO ()
370toFile filepath = BL.writeFile filepath . encode 533toFile filepath = BL.writeFile filepath . BE.encode
534
535{-----------------------------------------------------------------------
536-- URN
537-----------------------------------------------------------------------}
538
539-- | Namespace identifier determines the syntactic interpretation of
540-- namespace-specific string.
541type 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--
547btih :: NamespaceId
548btih = ["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--
555data 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
563instance 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--
571infohashURN :: InfoHash -> URN
572infohashURN = URN btih . longHex
573
574-- | Meaningless placeholder value.
575instance Default URN where
576 def = infohashURN def
577
578------------------------------------------------------------------------
579
580-- | Render URN to its text representation.
581renderURN :: URN -> Text
582renderURN URN {..}
583 = T.intercalate ":" $ "urn" : urnNamespace ++ [urnString]
584
585instance Pretty URN where
586 pretty = text . T.unpack . renderURN
587
588instance Show URN where
589 showsPrec n = showsPrec n . T.unpack . renderURN
590
591instance QueryValueLike URN where
592 toQueryValue = toQueryValue . renderURN
593 {-# INLINE toQueryValue #-}
594
595-----------------------------------------------------------------------
596
597unsnoc :: [a] -> Maybe ([a], a)
598unsnoc [] = Nothing
599unsnoc xs = Just (L.init xs, L.last xs)
600
601instance 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
614instance 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--
622parseURN :: Text -> Maybe URN
623parseURN = 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.
650data 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
683instance QueryValueLike Integer where
684 toQueryValue = toQueryValue . show
685
686instance QueryValueLike URI where
687 toQueryValue = toQueryValue . show
688
689instance 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
701instance QueryValueLike Magnet where
702 toQueryValue = toQueryValue . renderMagnet
703
704instance 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
725magnetScheme :: URI
726magnetScheme = URI
727 { uriScheme = "magnet:"
728 , uriAuthority = Nothing
729 , uriPath = ""
730 , uriQuery = ""
731 , uriFragment = ""
732 }
733
734isMagnetURI :: URI -> Bool
735isMagnetURI u = u { uriQuery = "" } == magnetScheme
736
737-- | Can be used instead of 'parseMagnet'.
738instance 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'.
744instance Convertible Magnet URI where
745 safeConvert m = pure $ magnetScheme
746 { uriQuery = BC.unpack $ renderQuery True $ toQuery m }
747
748instance 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.
756instance 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.
770nullMagnet :: InfoHash -> Magnet
771nullMagnet 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).
784simpleMagnet :: Torrent -> Magnet
785simpleMagnet 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--
793detailedMagnet :: Torrent -> Magnet
794detailedMagnet t @ Torrent {tInfoDict = InfoDict {..}, tAnnounce}
795 = (simpleMagnet t)
796 { exactLength = Just $ fromIntegral $ contentLength idLayoutInfo
797 , tracker = tAnnounce
798 }
799
800-----------------------------------------------------------------------
801
802parseMagnetStr :: String -> Maybe Magnet
803parseMagnetStr = either (const Nothing) Just . safeConvert
804
805renderMagnetStr :: Magnet -> String
806renderMagnetStr = show . (convert :: Magnet -> URI)
807
808instance Pretty Magnet where
809 pretty = PP.text . renderMagnetStr
810
811instance Show Magnet where
812 show = renderMagnetStr
813 {-# INLINE show #-}
814
815instance Read Magnet where
816 readsPrec _ xs
817 | Just m <- parseMagnetStr mstr = [(m, rest)]
818 | otherwise = []
819 where
820 (mstr, rest) = L.break (== ' ') xs
821
822instance 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--
830parseMagnet :: Text -> Maybe Magnet
831parseMagnet = parseMagnetStr . T.unpack
832{-# INLINE parseMagnet #-}
833
834-- | Render magnet link to urlencoded string
835renderMagnet :: Magnet -> Text
836renderMagnet = 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 #-}
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/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 #-}