summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSam Truzjan <pxqr.sta@gmail.com>2013-10-31 11:25:59 +0400
committerSam Truzjan <pxqr.sta@gmail.com>2013-10-31 11:25:59 +0400
commit01cef3fafc27d39d88c94cacdcd8e204c5f66b86 (patch)
tree01040aca19e49f4e7937383fef53b8c82bcec12b
parentc1fec260f47084300ac30de2e43d52966316a2c7 (diff)
Merge bittorrent package with torrent-content
-rw-r--r--.ghci7
-rw-r--r--.mailmap2
-rw-r--r--LICENSE43
-rw-r--r--TODO.org3
-rw-r--r--bench/TorrentFile.hs27
-rw-r--r--bittorrent.cabal152
-rw-r--r--changelog0
-rw-r--r--examples/FS.hs74
-rw-r--r--res/dapper-dvd-amd64.iso.torrentbin0 -> 64198 bytes
-rw-r--r--res/pkg.torrentbin0 -> 32113 bytes
-rw-r--r--src/Data/Torrent.hs273
-rw-r--r--src/Data/Torrent/InfoHash.hs115
-rw-r--r--src/Data/Torrent/Layout.hs273
-rw-r--r--src/Data/Torrent/Magnet.hs236
-rw-r--r--src/Data/Torrent/Piece.hs203
-rw-r--r--src/Data/Torrent/Tree.hs71
-rw-r--r--src/Network/BitTorrent.hs4
-rw-r--r--src/Network/BitTorrent/DHT/Protocol.hs5
-rw-r--r--src/Network/BitTorrent/Exchange/Protocol.hs2
-rw-r--r--src/Network/BitTorrent/Sessions.hs2
-rw-r--r--src/Network/BitTorrent/Sessions/Types.lhs1
-rw-r--r--src/Network/BitTorrent/Tracker.hs2
-rw-r--r--src/Network/BitTorrent/Tracker/HTTP.hs1
-rw-r--r--src/Network/BitTorrent/Tracker/Protocol.hs2
-rw-r--r--src/Network/BitTorrent/Tracker/UDP.hs1
-rw-r--r--src/System/Torrent/Storage.hs2
m---------sub/bencoding0
-rw-r--r--tests/Data/Torrent/InfoHashSpec.hs36
-rw-r--r--tests/Data/Torrent/MagnetSpec.hs44
-rw-r--r--tests/Data/Torrent/MetainfoSpec.hs76
-rw-r--r--tests/Spec.hs1
31 files changed, 1542 insertions, 116 deletions
diff --git a/.ghci b/.ghci
index c5d93fc0..cb115f34 100644
--- a/.ghci
+++ b/.ghci
@@ -1,7 +1,8 @@
1import Control.Lens
2import Data.BEncode
1import Data.Serialize as S 3import Data.Serialize as S
2 4import Data.Torrent
3import Network 5import Network
4import Network.Socket hiding (send, sendTo, recv, recvFrom) 6import Network.Socket hiding (send, sendTo, recv, recvFrom)
5import Network.Socket.ByteString 7import Network.Socket.ByteString
6 8import Network.BitTorrent.DHT.Protocol
7import Network.BitTorrent.DHT.Protocol \ No newline at end of file
diff --git a/.mailmap b/.mailmap
new file mode 100644
index 00000000..55e6f926
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,2 @@
1Sam Truzjan <pxqr.sta@gmail.com>
2Sam Truzjan <sta.cs.vsu@gmail.com>
diff --git a/LICENSE b/LICENSE
index 777f89d3..4c30139e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,19 +1,30 @@
1Copyright (c) 2012 Sam T. 1Copyright (c) 2013, Sam Truzjan
2 2
3Permission is hereby granted, free of charge, to any person obtaining a copy of 3All rights reserved.
4this software and associated documentation files (the "Software"), to deal in
5the Software without restriction, including without limitation the rights to
6use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7of the Software, and to permit persons to whom the Software is furnished to do
8so, subject to the following conditions:
9 4
10The above copyright notice and this permission notice shall be included in all 5Redistribution and use in source and binary forms, with or without
11copies or substantial portions of the Software. 6modification, are permitted provided that the following conditions are met:
12 7
13THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 8 * Redistributions of source code must retain the above copyright
14IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 9 notice, this list of conditions and the following disclaimer.
15FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 10
16AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 11 * Redistributions in binary form must reproduce the above
17LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 12 copyright notice, this list of conditions and the following
18OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 13 disclaimer in the documentation and/or other materials provided
19SOFTWARE. \ No newline at end of file 14 with the distribution.
15
16 * Neither the name of Sam Truzjan nor the names of other
17 contributors may be used to endorse or promote products derived
18 from this software without specific prior written permission.
19
20THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/TODO.org b/TODO.org
new file mode 100644
index 00000000..359bbecf
--- /dev/null
+++ b/TODO.org
@@ -0,0 +1,3 @@
1* TODO move PeerClient to Data.Torrent.Client
2* TODO Word64 for Progress fields
3* TODO rename TConnection -> Connection, etc
diff --git a/bench/TorrentFile.hs b/bench/TorrentFile.hs
new file mode 100644
index 00000000..e91a9c10
--- /dev/null
+++ b/bench/TorrentFile.hs
@@ -0,0 +1,27 @@
1{-# LANGUAGE BangPatterns #-}
2module Main (main) where
3
4import Data.BEncode
5import Data.ByteString as BS
6import Data.Torrent
7import Criterion.Main
8
9
10tinyPath :: FilePath
11tinyPath = "res/dapper-dvd-amd64.iso.torrent"
12
13largePath :: FilePath
14largePath = "res/pkg.torrent"
15
16decoder :: ByteString -> Torrent
17decoder bs = let Right r = decode bs in r
18
19main :: IO ()
20main = do
21 !tinyBin <- BS.readFile tinyPath
22 !largeBin <- BS.readFile largePath
23
24 defaultMain
25 [ bench "read/tiny" $ nf decoder tinyBin
26 , bench "read/large" $ nf decoder largeBin
27 ] \ No newline at end of file
diff --git a/bittorrent.cabal b/bittorrent.cabal
index fca04ef7..a7d9114d 100644
--- a/bittorrent.cabal
+++ b/bittorrent.cabal
@@ -1,17 +1,17 @@
1name: bittorrent 1name: bittorrent
2version: 0.1.0.0 2version: 0.0.0.0
3license: MIT 3license: BSD3
4license-file: LICENSE 4license-file: LICENSE
5author: Sam T. 5author: Sam Truzjan
6maintainer: Sam T. <pxqr.sta@gmail.com> 6maintainer: Sam Truzjan <pxqr.sta@gmail.com>
7copyright: (c) 2013, Sam T. 7copyright: (c) 2013, Sam Truzjan
8category: Network 8category: Network
9build-type: Simple 9build-type: Simple
10cabal-version: >= 1.10 10cabal-version: >= 1.10
11tested-with: GHC == 7.4.1 11tested-with: GHC == 7.4.1
12 , GHC == 7.6.3 12 , GHC == 7.6.3
13homepage: https://github.com/pxqr/bittorrent 13homepage: https://github.com/cobit/bittorrent
14bug-reports: https://github.com/pxqr/bittorrent/issues 14bug-reports: https://github.com/cobit/bittorrent/issues
15synopsis: Bittorrent protocol implementation. 15synopsis: Bittorrent protocol implementation.
16description: 16description:
17 17
@@ -20,20 +20,19 @@ description:
20 For more information see: 20 For more information see:
21 <https://github.com/pxqr/bittorrent/blob/master/README.md> 21 <https://github.com/pxqr/bittorrent/blob/master/README.md>
22 22
23 23extra-source-files: res/*
24extra-source-files: res/dapper-dvd-amd64.iso.torrent 24 , README.md
25-- not a source file, but we need to keep it in tarball for tests 25 , changelog
26
27 26
28source-repository head 27source-repository head
29 type: git 28 type: git
30 location: git://github.com/pxqr/bittorrent.git 29 location: git://github.com/cobit/bittorrent.git
31
32
33flag testing
34 description: Expose debug stuff in export declarations.
35 default: False
36 30
31source-repository this
32 type: git
33 location: git://github.com/cobit/bittorrent.git
34 branch: master
35 tag: v0.0.0.0
37 36
38library 37library
39 default-language: Haskell2010 38 default-language: Haskell2010
@@ -41,39 +40,40 @@ library
41 , OverloadedStrings 40 , OverloadedStrings
42 , RecordWildCards 41 , RecordWildCards
43 hs-source-dirs: src 42 hs-source-dirs: src
44 exposed-modules: Network.BitTorrent 43 exposed-modules: Data.Torrent
45 , Network.BitTorrent.Extension 44 , Data.Torrent.Bitfield
46 , Network.BitTorrent.Peer 45 , Data.Torrent.Block
47 , Network.BitTorrent.Tracker 46 , Data.Torrent.InfoHash
48 , Network.BitTorrent.Exchange 47 , Data.Torrent.Layout
49 , Network.BitTorrent.DHT 48 , Data.Torrent.Magnet
50 , Network.BitTorrent.Sessions 49 , Data.Torrent.Piece
51 other-modules: Network.BitTorrent.Sessions.Types 50 , Data.Torrent.Tree
52 51
53 if flag(testing) 52-- , System.IO.MMap.Fixed
54 exposed-modules: Network.BitTorrent.Exchange.Protocol 53-- , System.Torrent.Storage
55 , Network.BitTorrent.Tracker.Protocol 54
56 , Network.BitTorrent.Tracker.HTTP 55-- Network.BitTorrent
57 , Network.BitTorrent.Tracker.UDP 56-- , Network.BitTorrent.Extension
58 , Network.BitTorrent.DHT.Protocol 57-- , Network.BitTorrent.Peer
59 if !flag(testing) 58-- , Network.BitTorrent.Tracker
60 other-modules: Network.BitTorrent.Exchange.Protocol 59-- , Network.BitTorrent.Tracker.Protocol
61 , Network.BitTorrent.Tracker.Protocol 60-- , Network.BitTorrent.Tracker.HTTP
62 , Network.BitTorrent.Tracker.HTTP 61-- , Network.BitTorrent.Tracker.UDP
63 , Network.BitTorrent.Tracker.UDP 62-- , Network.BitTorrent.Exchange
64 , Network.BitTorrent.DHT.Protocol 63-- , Network.BitTorrent.Exchange.Protocol
65 64-- , Network.BitTorrent.DHT
65-- , Network.BitTorrent.DHT.Protocol
66-- , Network.BitTorrent.Sessions
67-- other-modules: Network.BitTorrent.Sessions.Types
66 build-depends: base == 4.* 68 build-depends: base == 4.*
67 69 , bits-extras
68 -- BitTorrent 70 , pretty
69 , bencoding >= 0.1.0.1
70 , krpc >= 0.1.1
71 , torrent-content
72 71
73 -- Control 72 -- Control
73 , deepseq
74 , lens
74 , mtl 75 , mtl
75 , resourcet 76 , resourcet
76 , lens
77 , transformers 77 , transformers
78 78
79 -- Concurrency 79 -- Concurrency
@@ -81,48 +81,54 @@ library
81 , BoundedChan >= 1.0.1.0 81 , BoundedChan >= 1.0.1.0
82 , stm >= 2.4 82 , stm >= 2.4
83 83
84 -- Conduits 84 -- Streaming
85 , conduit >= 1.0 85 , conduit >= 1.0
86 , network-conduit >= 1.0 86 , network-conduit >= 1.0
87 , cereal-conduit >= 0.5 87 , cereal-conduit >= 0.5
88 , binary-conduit 88 , binary-conduit
89 89
90 -- Data 90 -- Data & Data structures
91 , data-default
92 , bytestring >= 0.10.0.0 91 , bytestring >= 0.10.0.0
93 , containers >= 0.4 92 , containers >= 0.4
94 , unordered-containers 93 , data-default
94 , IntervalMap
95 , intset
95 , text >= 0.11.0 96 , text >= 0.11.0
97 , unordered-containers
98 , vector
96 99
97 -- Encoding/Serialization 100 -- Hashing
101 , cryptohash
102 , hashable
103
104 -- Codecs & Serialization
98 , aeson 105 , aeson
106 , base16-bytestring
107 , base32-bytestring
108 , bencoding >= 0.4
109 , binary
99 , cereal >= 0.3 110 , cereal >= 0.3
100 , binary >= 0.5
101 , urlencoded >= 0.4 111 , urlencoded >= 0.4
102 112
103 -- Time 113 -- Time
104 , time >= 0.1
105 , old-locale >= 1.0 114 , old-locale >= 1.0
115 , time >= 0.1
106 116
107 -- Network 117 -- Network
108 , network >= 2.4 118 , network >= 2.4
109 , HTTP >= 4000.2 119 , HTTP >= 4000.2
120 , krpc >= 0.3
110 121
111 -- System 122 -- System
112 , filepath >= 1
113 , directory >= 1 123 , directory >= 1
114 , entropy 124 , entropy
125 , filepath >= 1
126 , mmap
115 127
116 -- Misc
117 , pretty
118
119 if flag(testing)
120 cpp-options: -DTESTING
121 ghc-options: -Wall 128 ghc-options: -Wall
122 ghc-prof-options: 129 ghc-prof-options:
123 130
124 131
125
126test-suite properties 132test-suite properties
127 default-language: Haskell2010 133 default-language: Haskell2010
128 default-extensions: 134 default-extensions:
@@ -146,21 +152,17 @@ test-suite properties
146 , test-framework-hunit 152 , test-framework-hunit
147 153
148 , bencoding 154 , bencoding
149 , torrent-content
150 , bittorrent 155 , bittorrent
151 156
152 ghc-options: -Wall -fno-warn-orphans 157 ghc-options: -Wall -fno-warn-orphans
153 if !flag(testing)
154 buildable: False
155
156 158
157 159
158benchmark benchmarks 160benchmark benchmarks
159 default-language: Haskell2010 161 default-language: Haskell2010
160 default-extensions: 162 default-extensions:
161 type: exitcode-stdio-1.0 163 type: exitcode-stdio-1.0
162 main-is: Main.hs
163 hs-source-dirs: bench 164 hs-source-dirs: bench
165 main-is: Main.hs
164 build-depends: base 166 build-depends: base
165 , bytestring 167 , bytestring
166 , cereal 168 , cereal
@@ -169,24 +171,6 @@ benchmark benchmarks
169 , criterion 171 , criterion
170 , deepseq 172 , deepseq
171 173
172 , torrent-content
173 , bittorrent 174 , bittorrent
174 175
175 ghc-options: -O2 -Wall -fno-warn-orphans 176 ghc-options: -O2 -Wall -fno-warn-orphans
176 if !flag(testing)
177 buildable: False
178
179
180
181executable example
182 default-language: Haskell2010
183 main-is: Main.hs
184 hs-source-dirs: examples
185 build-depends: base == 4.*
186 , bittorrent
187 , mtl
188 ghc-options:
189 ghc-prof-options: -prof -rtsopts
190-- -threaded -eventlog
191 if !flag(testing)
192 buildable: False
diff --git a/changelog b/changelog
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/changelog
diff --git a/examples/FS.hs b/examples/FS.hs
new file mode 100644
index 00000000..550d85a7
--- /dev/null
+++ b/examples/FS.hs
@@ -0,0 +1,74 @@
1{-# LANGUAGE RecordWildCards #-}
2module Main (main) where
3
4import Control.Arrow
5import Data.ByteString.Char8 as BC
6import Data.List as L
7import Data.Map as M
8import Data.Torrent as T
9import Data.Torrent.Tree as T
10import System.Environment
11import System.Fuse
12import System.FilePath
13import System.Posix.Files
14
15
16defStat :: FileStat
17defStat = FileStat
18 { statEntryType = Unknown
19 , statFileMode = ownerReadMode
20 , statLinkCount = 2
21
22 , statFileOwner = 0
23 , statFileGroup = 0
24
25 , statSpecialDeviceID = 0
26
27 , statFileSize = 0
28 , statBlocks = 0
29
30 , statAccessTime = 0
31 , statModificationTime = 0
32 , statStatusChangeTime = 0
33 }
34
35dirStat :: FileStat
36dirStat = defStat {
37 statEntryType = Directory
38 }
39
40type Result a = IO (Either Errno a)
41type Result' = IO Errno
42
43fsGetFileStat :: Torrent -> FilePath -> Result FileStat
44fsGetFileStat _ path = return $ Right dirStat
45
46fsOpenDirectory :: Torrent -> FilePath -> Result'
47fsOpenDirectory _ _ = return eOK
48
49fsReadDirectory :: Torrent -> FilePath -> Result [(FilePath, FileStat)]
50fsReadDirectory Torrent {tInfoDict = InfoDict {..}} path
51 | Just cs <- T.lookupDir (L.tail (splitDirectories path)) tree =
52 return $ Right $ L.map (BC.unpack *** const defStat) cs
53 | otherwise = return $ Left eNOENT
54 where
55 tree = build $ idLayoutInfo
56
57fsReleaseDirectory :: Torrent -> FilePath -> Result'
58fsReleaseDirectory _ _ = return eOK
59
60exfsOps :: Torrent -> FuseOperations ()
61exfsOps t = defaultFuseOps
62 { fuseGetFileStat = fsGetFileStat t
63
64 , fuseOpenDirectory = fsOpenDirectory t
65 , fuseReadDirectory = fsReadDirectory t
66 , fuseReleaseDirectory = fsReleaseDirectory t
67 }
68
69main :: IO ()
70main = do
71 x : xs <- getArgs
72 t <- fromFile x
73 withArgs xs $ do
74 fuseMain (exfsOps t) defaultExceptionHandler \ No newline at end of file
diff --git a/res/dapper-dvd-amd64.iso.torrent b/res/dapper-dvd-amd64.iso.torrent
new file mode 100644
index 00000000..5713344b
--- /dev/null
+++ b/res/dapper-dvd-amd64.iso.torrent
Binary files differ
diff --git a/res/pkg.torrent b/res/pkg.torrent
new file mode 100644
index 00000000..be89e9e0
--- /dev/null
+++ b/res/pkg.torrent
Binary files differ
diff --git a/src/Data/Torrent.hs b/src/Data/Torrent.hs
new file mode 100644
index 00000000..15ada35f
--- /dev/null
+++ b/src/Data/Torrent.hs
@@ -0,0 +1,273 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- Torrent file contains metadata about files and folders but not
9-- content itself. The files are bencoded dictionaries. There is
10-- also other info which is used to help join the swarm.
11--
12-- This module provides torrent metainfo serialization and info hash
13-- extraction.
14--
15-- For more info see:
16-- <http://www.bittorrent.org/beps/bep_0003.html#metainfo-files>,
17-- <https://wiki.theory.org/BitTorrentSpecification#Metainfo_File_Structure>
18--
19{-# LANGUAGE CPP #-}
20{-# LANGUAGE FlexibleInstances #-}
21{-# LANGUAGE BangPatterns #-}
22{-# LANGUAGE GeneralizedNewtypeDeriving #-}
23{-# LANGUAGE DeriveDataTypeable #-}
24{-# LANGUAGE TemplateHaskell #-}
25{-# OPTIONS -fno-warn-orphans #-}
26-- TODO refine interface
27module Data.Torrent
28 ( -- * Info dictionary
29 InfoDict (..)
30 , infohash
31 , layoutInfo
32 , pieceInfo
33 , isPrivate
34
35 -- * Torrent file
36 , Torrent(..)
37 , announce
38 , announceList
39 , comment
40 , createdBy
41 , creationDate
42 , encoding
43 , infoDict
44 , publisher
45 , publisherURL
46 , signature
47
48 , nullTorrent
49
50 -- * IO
51 , torrentExt
52 , isTorrentPath
53 , fromFile
54 , toFile
55
56{-
57 , nullTorrent
58 , mktorrent
59
60
61-}
62 ) where
63
64import Prelude hiding (sum)
65
66import Control.Applicative
67import Control.DeepSeq
68import Control.Exception
69import Control.Lens
70
71import Data.Aeson.TH
72import Data.BEncode as BE
73import Data.BEncode.Types as BE
74import Data.ByteString as BS
75import qualified Data.ByteString.Char8 as BC (pack, unpack)
76import qualified Data.ByteString.Lazy as BL
77import Data.Char
78import Data.Hashable as Hashable
79import qualified Data.List as L
80import Data.Text (Text)
81import Data.Time.Clock.POSIX
82import Data.Typeable
83import Network.URI
84import System.FilePath
85
86import Data.Torrent.InfoHash as IH
87import Data.Torrent.Layout
88import Data.Torrent.Piece
89
90
91{-----------------------------------------------------------------------
92-- Info dictionary
93-----------------------------------------------------------------------}
94
95{- note that info hash is actually reduntant field
96 but it's better to keep it here to avoid heavy recomputations
97-}
98
99-- | Info part of the .torrent file contain info about each content file.
100data InfoDict = InfoDict
101 { idInfoHash :: !InfoHash
102 -- ^ SHA1 hash of the (other) 'DictInfo' fields.
103 , idLayoutInfo :: !LayoutInfo
104 , idPieceInfo :: !PieceInfo
105 , idPrivate :: !Bool
106 -- ^ If set the client MUST publish its presence to get other
107 -- peers ONLY via the trackers explicity described in the
108 -- metainfo file.
109 --
110 -- BEP 27: <http://www.bittorrent.org/beps/bep_0027.html>
111 } deriving (Show, Read, Eq, Typeable)
112
113$(deriveJSON (L.map toLower . L.dropWhile isLower) ''InfoDict)
114
115makeLensesFor
116 [ ("idInfoHash" , "infohash" )
117 , ("idLayoutInfo", "layoutInfo")
118 , ("idPieceInfo" , "pieceInfo" )
119 , ("idPrivate" , "isPrivate" )
120 ]
121 ''InfoDict
122
123instance NFData InfoDict where
124 rnf InfoDict {..} = rnf idLayoutInfo
125
126instance Hashable InfoDict where
127 hash = Hashable.hash . idInfoHash
128 {-# INLINE hash #-}
129
130getPrivate :: Get Bool
131getPrivate = (Just True ==) <$>? "private"
132
133putPrivate :: Bool -> BDict -> BDict
134putPrivate False = id
135putPrivate True = \ cont -> "private" .=! True .: cont
136
137instance BEncode InfoDict where
138 toBEncode InfoDict {..} = toDict $
139 putLayoutInfo idLayoutInfo $
140 putPieceInfo idPieceInfo $
141 putPrivate idPrivate $
142 endDict
143
144 fromBEncode dict = (`fromDict` dict) $ do
145 InfoDict ih <$> getLayoutInfo
146 <*> getPieceInfo
147 <*> getPrivate
148 where
149 ih = IH.hashlazy (encode dict)
150
151{-----------------------------------------------------------------------
152-- Torrent info
153-----------------------------------------------------------------------}
154
155-- | Metainfo about particular torrent.
156data Torrent = Torrent
157 { tAnnounce :: !URI
158 -- ^ The URL of the tracker.
159
160 , tAnnounceList :: !(Maybe [[URI]])
161 -- ^ Announce list add multiple tracker support.
162 --
163 -- BEP 12: <http://www.bittorrent.org/beps/bep_0012.html>
164
165 , tComment :: !(Maybe Text)
166 -- ^ Free-form comments of the author.
167
168 , tCreatedBy :: !(Maybe Text)
169 -- ^ Name and version of the program used to create the .torrent.
170
171 , tCreationDate :: !(Maybe POSIXTime)
172 -- ^ Creation time of the torrent, in standard UNIX epoch.
173
174 , tEncoding :: !(Maybe Text)
175 -- ^ String encoding format used to generate the pieces part of
176 -- the info dictionary in the .torrent metafile.
177
178 , tInfoDict :: !InfoDict
179 -- ^ Info about each content file.
180
181 , tPublisher :: !(Maybe URI)
182 -- ^ Containing the RSA public key of the publisher of the
183 -- torrent. Private counterpart of this key that has the
184 -- authority to allow new peers onto the swarm.
185
186 , tPublisherURL :: !(Maybe URI)
187 , tSignature :: !(Maybe ByteString)
188 -- ^ The RSA signature of the info dictionary (specifically, the
189 -- encrypted SHA-1 hash of the info dictionary).
190 } deriving (Show, Eq, Typeable)
191
192makeLensesFor
193 [ ("tAnnounce" , "announce" )
194 , ("tAnnounceList", "announceList")
195 , ("tComment" , "comment" )
196 , ("tCreatedBy" , "createdBy" )
197 , ("tCreationDate", "creationDate")
198 , ("tEncoding" , "encoding" )
199 , ("tInfoDict" , "infoDict" )
200 , ("tPublisher" , "publisher" )
201 , ("tPublisherURL", "publisherURL")
202 , ("tSignature" , "signature" )
203 ]
204 ''Torrent
205
206instance NFData Torrent where
207 rnf Torrent {..} = rnf tInfoDict
208
209-- TODO move to bencoding
210instance BEncode URI where
211 toBEncode uri = toBEncode (BC.pack (uriToString id uri ""))
212 {-# INLINE toBEncode #-}
213
214 fromBEncode (BString s) | Just url <- parseURI (BC.unpack s) = return url
215 fromBEncode b = decodingError $ "url <" ++ show b ++ ">"
216 {-# INLINE fromBEncode #-}
217
218-- TODO move to bencoding
219instance BEncode POSIXTime where
220 toBEncode pt = toBEncode (floor pt :: Integer)
221 fromBEncode (BInteger i) = return $ fromIntegral i
222 fromBEncode _ = decodingError $ "POSIXTime"
223
224instance BEncode Torrent where
225 toBEncode Torrent {..} = toDict $
226 "announce" .=! tAnnounce
227 .: "announce-list" .=? tAnnounceList
228 .: "comment" .=? tComment
229 .: "created by" .=? tCreatedBy
230 .: "creation date" .=? tCreationDate
231 .: "encoding" .=? tEncoding
232 .: "info" .=! tInfoDict
233 .: "publisher" .=? tPublisher
234 .: "publisher-url" .=? tPublisherURL
235 .: "signature" .=? tSignature
236 .: endDict
237
238 fromBEncode = fromDict $ do
239 Torrent <$>! "announce"
240 <*>? "announce-list"
241 <*>? "comment"
242 <*>? "created by"
243 <*>? "creation date"
244 <*>? "encoding"
245 <*>! "info"
246 <*>? "publisher"
247 <*>? "publisher-url"
248 <*>? "signature"
249
250-- | A simple torrent contains only required fields.
251nullTorrent :: URI -> InfoDict -> Torrent
252nullTorrent ann info = Torrent
253 ann Nothing Nothing Nothing Nothing Nothing
254 info Nothing Nothing Nothing
255
256-- | Extension usually used for torrent metafiles.
257torrentExt :: String
258torrentExt = "torrent"
259
260-- | Test if this path has proper extension.
261isTorrentPath :: FilePath -> Bool
262isTorrentPath filepath = takeExtension filepath == extSeparator : torrentExt
263
264-- | Read and decode a .torrent file.
265fromFile :: FilePath -> IO Torrent
266fromFile filepath = do
267 contents <- BS.readFile filepath
268 case decode contents of
269 Right !t -> return t
270 Left msg -> throwIO $ userError $ msg ++ " while reading torrent file"
271
272toFile :: FilePath -> Torrent -> IO ()
273toFile filepath = BL.writeFile filepath . encode
diff --git a/src/Data/Torrent/InfoHash.hs b/src/Data/Torrent/InfoHash.hs
new file mode 100644
index 00000000..71ea0260
--- /dev/null
+++ b/src/Data/Torrent/InfoHash.hs
@@ -0,0 +1,115 @@
1{-# LANGUAGE FlexibleInstances #-}
2{-# LANGUAGE GeneralizedNewtypeDeriving #-}
3module Data.Torrent.InfoHash
4 ( -- * Info hash
5 InfoHash(..)
6 , addHashToURI
7 , ppInfoHash
8
9
10 , Data.Torrent.InfoHash.hash
11 , Data.Torrent.InfoHash.hashlazy
12 ) where
13
14import Control.Applicative
15import Control.Monad
16import qualified Crypto.Hash.SHA1 as C
17import Data.Aeson
18import Data.BEncode
19import Data.ByteString as BS
20import Data.ByteString.Char8 as BC
21import Data.ByteString.Lazy as BL
22import Data.ByteString.Base16 as Base16
23import qualified Data.ByteString.Lazy.Builder as B
24import qualified Data.ByteString.Lazy.Builder.ASCII as B
25import Data.Char
26import Data.List as L
27import Data.Hashable as Hashable
28import Data.URLEncoded as URL
29import Data.Serialize
30import Data.String
31import Network.URI
32import Numeric
33import Text.ParserCombinators.ReadP as P
34import Text.PrettyPrint
35
36
37-- | Exactly 20 bytes long SHA1 hash of the info part of torrent file.
38newtype InfoHash = InfoHash { getInfoHash :: BS.ByteString }
39 deriving (Eq, Ord, ToJSON, FromJSON)
40
41-- | for hex encoded strings
42instance Show InfoHash where
43 show = render . ppInfoHash
44
45-- | for hex encoded strings
46instance Read InfoHash where
47 readsPrec _ = readP_to_S $ do
48 str <- replicateM 40 (satisfy isHexDigit)
49 return $ InfoHash $ decodeIH str
50 where
51 decodeIH = BS.pack . L.map fromHex . pair
52 fromHex (a, b) = read $ '0' : 'x' : a : b : []
53
54 pair (a : b : xs) = (a, b) : pair xs
55 pair _ = []
56
57-- | for base16 (hex) encoded strings
58instance IsString InfoHash where
59 fromString str
60 | L.length str == 40
61 , (ihStr, inv) <- Base16.decode $ BC.pack str
62 = if BS.length inv == 0 then InfoHash ihStr
63 else error "fromString: invalid infohash string"
64 | otherwise = error "fromString: invalid infohash string length"
65
66instance Hashable InfoHash where
67 hash = Hashable.hash . getInfoHash
68
69instance BEncode InfoHash where
70 toBEncode = toBEncode . getInfoHash
71 fromBEncode be = InfoHash <$> fromBEncode be
72
73instance Serialize InfoHash where
74 put = putByteString . getInfoHash
75 get = InfoHash <$> getBytes 20
76
77instance URLShow InfoHash where
78 urlShow = show
79
80-- | Hash strict bytestring using SHA1 algorithm.
81hash :: BS.ByteString -> InfoHash
82hash = InfoHash . C.hash
83
84-- | Hash lazy bytestring using SHA1 algorithm.
85hashlazy :: BL.ByteString -> InfoHash
86hashlazy = InfoHash . C.hashlazy
87
88-- | Pretty print info hash in hexadecimal format.
89ppInfoHash :: InfoHash -> Doc
90ppInfoHash = text . BC.unpack . ppHex . getInfoHash
91
92ppHex :: BS.ByteString -> BS.ByteString
93ppHex = BL.toStrict . B.toLazyByteString . B.byteStringHexFixed
94
95-- | Add query info hash parameter to uri.
96--
97-- > info_hash=<url_encoded_info_hash>
98--
99addHashToURI :: URI -> InfoHash -> URI
100addHashToURI uri s = uri {
101 uriQuery = uriQuery uri ++ mkPref (uriQuery uri) ++
102 "info_hash=" ++ rfc1738Encode (BC.unpack (getInfoHash s))
103 }
104 where
105 mkPref [] = "?"
106 mkPref ('?' : _) = "&"
107 mkPref _ = error "addHashToURI"
108
109 rfc1738Encode = L.concatMap (\c -> if unreservedS c then [c] else encodeHex c)
110 where
111 unreservedS = (`L.elem` chars)
112 chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_./"
113 encodeHex c = '%' : pHex c
114 pHex c = let p = (showHex . ord $ c) ""
115 in if L.length p == 1 then '0' : p else p
diff --git a/src/Data/Torrent/Layout.hs b/src/Data/Torrent/Layout.hs
new file mode 100644
index 00000000..409426be
--- /dev/null
+++ b/src/Data/Torrent/Layout.hs
@@ -0,0 +1,273 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8{-# LANGUAGE BangPatterns #-}
9{-# LANGUAGE FlexibleInstances #-}
10{-# LANGUAGE StandaloneDeriving #-}
11{-# LANGUAGE GeneralizedNewtypeDeriving #-}
12{-# LANGUAGE DeriveDataTypeable #-}
13{-# LANGUAGE TemplateHaskell #-}
14{-# OPTIONS -fno-warn-orphans #-}
15module Data.Torrent.Layout
16 ( -- * File attribytes
17 FileOffset
18 , FileSize
19
20 -- * Single file info
21 , FileInfo (..)
22 , fileLength
23 , filePath
24 , fileMD5Sum
25
26 -- * File layout
27 , LayoutInfo (..)
28 , singleFile
29 , multiFile
30 , rootDirName
31 , isSingleFile
32 , isMultiFile
33 , fileNumber
34 , contentLength
35 , blockCount
36
37 -- * Flat file layout
38 , Layout
39 , flatLayout
40 , accumOffsets
41 , fileOffset
42
43 -- * Internal
44 , getLayoutInfo
45 , putLayoutInfo
46 ) where
47
48import Control.Applicative
49import Control.DeepSeq
50import Control.Lens
51import Data.Aeson.TH
52import Data.Aeson.Types (FromJSON, ToJSON)
53import Data.BEncode
54import Data.BEncode.Types
55import Data.ByteString as BS
56import Data.ByteString.Char8 as BC
57import Data.Char
58import Data.List as L
59import Data.Typeable
60import System.FilePath
61import System.Posix.Types
62
63
64{-----------------------------------------------------------------------
65-- File attribytes
66-----------------------------------------------------------------------}
67
68type FileSize = FileOffset
69
70deriving instance FromJSON FileOffset
71deriving instance ToJSON FileOffset
72deriving instance BEncode FileOffset
73
74{-----------------------------------------------------------------------
75-- File info both either from info dict or file list
76-----------------------------------------------------------------------}
77
78-- | Contain info about one single file.
79data FileInfo a = FileInfo {
80 fiLength :: {-# UNPACK #-} !FileSize
81 -- ^ Length of the file in bytes.
82
83 -- TODO unpacked MD5 sum
84 , fiMD5Sum :: !(Maybe ByteString)
85 -- ^ 32 character long MD5 sum of the file. Used by third-party
86 -- tools, not by bittorrent protocol itself.
87
88 , fiName :: !a
89 -- ^ One or more string elements that together represent the
90 -- path and filename. Each element in the list corresponds to
91 -- either a directory name or (in the case of the last
92 -- element) the filename. For example, the file:
93 --
94 -- > "dir1/dir2/file.ext"
95 --
96 -- would consist of three string elements:
97 --
98 -- > ["dir1", "dir2", "file.ext"]
99 --
100 } deriving (Show, Read, Eq, Typeable)
101
102$(deriveJSON (L.map toLower . L.dropWhile isLower) ''FileInfo)
103
104makeLensesFor
105 [ ("fiLength", "fileLength")
106 , ("fiMD5Sum", "fileMD5Sum")
107 , ("fiName" , "filePath" )
108 ]
109 ''FileInfo
110
111instance NFData a => NFData (FileInfo a) where
112 rnf FileInfo {..} = rnf fiName
113 {-# INLINE rnf #-}
114
115instance BEncode (FileInfo [ByteString]) where
116 toBEncode FileInfo {..} = toDict $
117 "length" .=! fiLength
118 .: "md5sum" .=? fiMD5Sum
119 .: "path" .=! fiName
120 .: endDict
121 {-# INLINE toBEncode #-}
122
123 fromBEncode = fromDict $ do
124 FileInfo <$>! "length"
125 <*>? "md5sum"
126 <*>! "path"
127 {-# INLINE fromBEncode #-}
128
129type Put a = a -> BDict -> BDict
130
131putFileInfoSingle :: Put (FileInfo ByteString)
132putFileInfoSingle FileInfo {..} cont =
133 "length" .=! fiLength
134 .: "md5sum" .=? fiMD5Sum
135 .: "name" .=! fiName
136 .: cont
137
138getFileInfoSingle :: Get (FileInfo ByteString)
139getFileInfoSingle = do
140 FileInfo <$>! "length"
141 <*>? "md5sum"
142 <*>! "name"
143
144instance BEncode (FileInfo ByteString) where
145 toBEncode = toDict . (`putFileInfoSingle` endDict)
146 {-# INLINE toBEncode #-}
147
148 fromBEncode = fromDict getFileInfoSingle
149 {-# INLINE fromBEncode #-}
150
151{-----------------------------------------------------------------------
152-- Original torrent file layout info
153-----------------------------------------------------------------------}
154
155data LayoutInfo
156 = SingleFile
157 { liFile :: !(FileInfo ByteString)
158 }
159 | MultiFile
160 { -- | List of the all files that torrent contains.
161 liFiles :: ![FileInfo [ByteString]]
162
163 -- | The /suggested/ name of the root directory in which to
164 -- store all the files.
165 , liDirName :: !ByteString
166 } deriving (Show, Read, Eq, Typeable)
167
168$(deriveJSON (L.map toLower . L.dropWhile isLower) ''LayoutInfo)
169
170makeLensesFor
171 [ ("liFile" , "singleFile" )
172 , ("liFiles" , "multiFile" )
173 , ("liDirName", "rootDirName")
174 ]
175 ''LayoutInfo
176
177instance NFData LayoutInfo where
178 rnf SingleFile {..} = ()
179 rnf MultiFile {..} = rnf liFiles
180
181getLayoutInfo :: Get LayoutInfo
182getLayoutInfo = single <|> multi
183 where
184 single = SingleFile <$> getFileInfoSingle
185 multi = MultiFile <$>! "files" <*>! "name"
186
187putLayoutInfo :: Put LayoutInfo
188putLayoutInfo SingleFile {..} = putFileInfoSingle liFile
189putLayoutInfo MultiFile {..} = \ cont ->
190 "files" .=! liFiles
191 .: "name" .=! liDirName
192 .: cont
193
194instance BEncode LayoutInfo where
195 toBEncode = toDict . (`putLayoutInfo` endDict)
196 fromBEncode = fromDict getLayoutInfo
197
198-- | Test if this is single file torrent.
199isSingleFile :: LayoutInfo -> Bool
200isSingleFile SingleFile {} = True
201isSingleFile _ = False
202{-# INLINE isSingleFile #-}
203
204-- | Test if this is multifile torrent.
205isMultiFile :: LayoutInfo -> Bool
206isMultiFile MultiFile {} = True
207isMultiFile _ = False
208{-# INLINE isMultiFile #-}
209
210-- | Find sum of sizes of the all torrent files.
211contentLength :: LayoutInfo -> FileSize
212contentLength SingleFile { liFile = FileInfo {..} } = fiLength
213contentLength MultiFile { liFiles = tfs } = sum (L.map fiLength tfs)
214
215-- | Get count of all files in torrent.
216fileNumber :: LayoutInfo -> Int
217fileNumber SingleFile {..} = 1
218fileNumber MultiFile {..} = L.length liFiles
219
220-- | Find number of blocks of the specified size. If torrent size is
221-- not a multiple of block size then the count is rounded up.
222blockCount :: Int -- ^ Block size.
223 -> LayoutInfo -- ^ Torrent content info.
224 -> Int -- ^ Number of blocks.
225blockCount blkSize ci = contentLength ci `sizeInBase` blkSize
226
227{-----------------------------------------------------------------------
228-- Flat layout
229-----------------------------------------------------------------------}
230
231-- | File layout specifies the order and the size of each file in the
232-- storage. Note that order of files is highly important since we
233-- coalesce all the files in the given order to get the linear block
234-- address space.
235--
236type Layout a = [(FilePath, a)]
237
238-- | Extract files layout from torrent info with the given root path.
239flatLayout
240 :: FilePath -- ^ Root path for the all torrent files.
241 -> LayoutInfo -- ^ Torrent content information.
242 -> Layout FileSize -- ^ The all file paths prefixed with the given root.
243flatLayout prefixPath SingleFile { liFile = FileInfo {..} }
244 = [(prefixPath </> BC.unpack fiName, fiLength)]
245flatLayout prefixPath MultiFile {..} = L.map mkPath liFiles
246 where -- TODO use utf8 encoding in name
247 mkPath FileInfo {..} = (path, fiLength)
248 where
249 path = prefixPath </> BC.unpack liDirName
250 </> joinPath (L.map BC.unpack fiName)
251
252accumOffsets :: Layout FileSize -> Layout FileOffset
253accumOffsets = go 0
254 where
255 go !_ [] = []
256 go !offset ((n, s) : xs) = (n, offset) : go (offset + s) xs
257
258-- | Gives global offset of a content file for a given full path.
259fileOffset :: FilePath -> Layout FileOffset -> Maybe FileOffset
260fileOffset = lookup
261{-# INLINE fileOffset #-}
262
263{-----------------------------------------------------------------------
264-- Internal utilities
265-----------------------------------------------------------------------}
266
267-- | Divide and round up.
268sizeInBase :: Integral a => a -> Int -> Int
269sizeInBase n b = fromIntegral (n `div` fromIntegral b) + align
270 where
271 align = if n `mod` fromIntegral b == 0 then 0 else 1
272{-# SPECIALIZE sizeInBase :: Int -> Int -> Int #-}
273{-# SPECIALIZE sizeInBase :: Integer -> Int -> Int #-}
diff --git a/src/Data/Torrent/Magnet.hs b/src/Data/Torrent/Magnet.hs
new file mode 100644
index 00000000..df928b66
--- /dev/null
+++ b/src/Data/Torrent/Magnet.hs
@@ -0,0 +1,236 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- Parsing and rendering of magnet URIs.
9--
10-- For more info see:
11-- <http://magnet-uri.sourceforge.net/magnet-draft-overview.txt>
12--
13-- Bittorrent specific info:
14-- <http://www.bittorrent.org/beps/bep_0009.html>
15--
16module Data.Torrent.Magnet
17 ( -- * Magnet
18 Magnet(..)
19 , nullMagnet
20 , parseMagnet
21 , renderMagnet
22
23 -- ** Extra
24 , fromURI
25 , toURI
26 ) where
27
28import Control.Applicative
29import Control.Monad
30import Data.ByteString as BS
31import Data.ByteString.Base16 as Base16
32import Data.ByteString.Base32 as Base32
33import Data.Map as M
34import Data.Maybe
35import Data.List as L
36import Data.URLEncoded as URL
37import Data.String
38import Data.Text as T
39import Data.Text.Encoding as T
40import Network.URI
41import Text.Read
42
43import Data.Torrent.InfoHash
44
45
46{-----------------------------------------------------------------------
47-- URN
48-----------------------------------------------------------------------}
49
50type NamespaceId = [Text]
51
52btih :: NamespaceId
53btih = ["btih"]
54
55-- | Uniform Resource Name - location-independent, resource
56-- identifier.
57data URN = URN
58 { urnNamespace :: NamespaceId
59 , urnString :: Text
60 } deriving (Eq, Ord)
61
62instance Show URN where
63 showsPrec n = showsPrec n . T.unpack . renderURN
64
65instance IsString URN where
66 fromString = fromMaybe def . parseURN . T.pack
67 where
68 def = error "unable to parse URN"
69
70instance URLShow URN where
71 urlShow = T.unpack . renderURN
72
73parseURN :: Text -> Maybe URN
74parseURN str = case T.split (== ':') str of
75 uriScheme : body
76 | T.toLower uriScheme == "urn" -> mkURN body
77 | otherwise -> Nothing
78 [] -> Nothing
79 where
80 mkURN [] = Nothing
81 mkURN xs = Just $ URN
82 { urnNamespace = L.init xs
83 , urnString = L.last xs
84 }
85
86renderURN :: URN -> Text
87renderURN URN {..}
88 = T.intercalate ":" $ "urn" : urnNamespace ++ [urnString]
89
90urnToInfoHash :: URN -> Maybe InfoHash
91urnToInfoHash (URN {..})
92 | urnNamespace /= btih = Nothing
93 | hashLen == 20 = Just $ InfoHash hashStr
94 | hashLen == 32 = Just $ InfoHash $ Base32.decode hashStr
95 | hashLen == 40 = let (ihStr, inv) = Base16.decode hashStr
96 in if BS.length inv == 0
97 then Just $ InfoHash ihStr
98 else Nothing
99 | otherwise = Nothing
100 where
101 hashLen = BS.length hashStr
102 hashStr = T.encodeUtf8 urnString
103
104infoHashToURN :: InfoHash -> URN
105infoHashToURN = URN btih . T.pack . show
106
107{-----------------------------------------------------------------------
108-- Magnet
109-----------------------------------------------------------------------}
110
111-- TODO multiple exact topics
112-- TODO supplement
113
114-- | An URI used to identify torrent.
115data Magnet = Magnet
116 { -- | Resource hash.
117 exactTopic :: !InfoHash
118 -- | Might be used to display name while waiting for metadata.
119 , displayName :: Maybe Text
120 -- | Size of the resource in bytes.
121 , exactLength :: Maybe Integer
122
123 , manifest :: Maybe String
124 -- | Search string.
125 , keywordTopic :: Maybe String
126
127 , acceptableSource :: Maybe URI
128 , exactSource :: Maybe URI
129
130 , tracker :: Maybe URI
131
132 , supplement :: Map Text Text
133 } deriving (Eq, Ord)
134
135instance Show Magnet where
136 show = renderMagnet
137 {-# INLINE show #-}
138
139instance Read Magnet where
140 readsPrec _ xs
141 | Just m <- parseMagnet mstr = [(m, rest)]
142 | otherwise = []
143 where
144 (mstr, rest) = L.break (== ' ') xs
145
146instance IsString Magnet where
147 fromString = fromMaybe def . parseMagnet
148 where
149 def = error "unable to parse magnet"
150
151instance URLEncode Magnet where
152 urlEncode = toQuery
153 {-# INLINE urlEncode #-}
154
155-- | Set exact topic only, other params are empty.
156nullMagnet :: InfoHash -> Magnet
157nullMagnet u = Magnet
158 { exactTopic = u
159 , displayName = Nothing
160 , exactLength = Nothing
161 , manifest = Nothing
162 , keywordTopic = Nothing
163 , acceptableSource = Nothing
164 , exactSource = Nothing
165 , tracker = Nothing
166 , supplement = M.empty
167 }
168
169fromQuery :: URLEncoded -> Either String Magnet
170fromQuery q
171 | Just urnStr <- URL.lookup ("xt" :: String) q
172 , Just urn <- parseURN $ T.pack urnStr
173 , Just infoHash <- urnToInfoHash urn
174 = return $ Magnet
175 { exactTopic = infoHash
176 , displayName = T.pack <$> URL.lookup ("dn" :: String) q
177 , exactLength = readMaybe =<< URL.lookup ("xl" :: String) q
178
179 , manifest = URL.lookup ("mt" :: String) q
180 , keywordTopic = URL.lookup ("kt" :: String) q
181
182 , acceptableSource = parseURI =<< URL.lookup ("as" :: String) q
183 , exactSource = parseURI =<< URL.lookup ("xs" :: String) q
184
185 , tracker = parseURI =<< URL.lookup ("tr" :: String) q
186 , supplement = M.empty
187 }
188
189 | otherwise = Left "exact topic not defined"
190
191toQuery :: Magnet -> URLEncoded
192toQuery Magnet {..}
193 = s "xt" %= infoHashToURN exactTopic
194 %& s "dn" %=? (T.unpack <$> displayName)
195 %& s "xl" %=? exactLength
196 %& s "mt" %=? manifest
197 %& s "kt" %=? keywordTopic
198 %& s "as" %=? acceptableSource
199 %& s "xs" %=? exactSource
200 %& s "tr" %=? tracker
201 where
202 s :: String -> String; s = id
203
204magnetScheme :: URI
205magnetScheme = URI
206 { uriScheme = "magnet:"
207 , uriAuthority = Nothing
208 , uriPath = ""
209 , uriQuery = ""
210 , uriFragment = ""
211 }
212
213isMagnetURI :: URI -> Bool
214isMagnetURI u = u { uriQuery = "" } == magnetScheme
215
216-- | The same as 'parseMagnet' but useful if you alread have a parsed
217-- uri.
218fromURI :: URI -> Either String Magnet
219fromURI u @ URI {..}
220 | not (isMagnetURI u) = Left "this is not a magnet link"
221 | otherwise = importURI u >>= fromQuery
222
223-- | The same as 'renderMagnet' but useful if you need an uri.
224toURI :: Magnet -> URI
225toURI m = magnetScheme %? urlEncode m
226
227etom :: Either a b -> Maybe b
228etom = either (const Nothing) Just
229
230-- | Try to parse magnet link from urlencoded string.
231parseMagnet :: String -> Maybe Magnet
232parseMagnet = parseURI >=> etom . fromURI
233
234-- | Render magnet link to urlencoded string
235renderMagnet :: Magnet -> String
236renderMagnet = show . toURI
diff --git a/src/Data/Torrent/Piece.hs b/src/Data/Torrent/Piece.hs
new file mode 100644
index 00000000..ea4e6253
--- /dev/null
+++ b/src/Data/Torrent/Piece.hs
@@ -0,0 +1,203 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8{-# LANGUAGE TemplateHaskell #-}
9{-# LANGUAGE DeriveDataTypeable #-}
10{-# LANGUAGE GeneralizedNewtypeDeriving #-}
11module Data.Torrent.Piece
12 ( -- * Piece attributes
13 -- ** Piece size
14 PieceSize (..)
15 , defaultBlockSize -- TODO use data-default
16 , optimalPieceCount
17 , defaultPieceSize -- TODO use data-default
18
19 -- ** Piece index
20 , PieceIx
21
22 -- * Piece data
23 , Piece (..)
24 , ppPiece
25
26 -- * Piece control
27 , PieceInfo (..)
28 , ppPieceInfo
29 , pieceLength
30 , pieceHashes
31 , pieceHash
32 , pieceCount
33 , checkPieceLazy
34
35
36 -- * Internal
37 , getPieceInfo
38 , putPieceInfo
39 ) where
40
41import Control.DeepSeq
42import Control.Lens
43import qualified Crypto.Hash.SHA1 as SHA1
44import Data.Aeson (ToJSON, FromJSON)
45import Data.Aeson.TH
46import Data.BEncode
47import Data.BEncode.Types
48import Data.Bits
49import Data.Bits.Extras
50import Data.ByteString as BS
51import qualified Data.ByteString.Lazy as BL
52import Data.Char
53import Data.Int
54import Data.List as L
55import Data.Typeable
56import Text.PrettyPrint
57
58
59class Lint a where
60 lint :: a -> Either String a
61
62type PieceCount = Int -- TODO newtype
63type PieceIx = Int -- TODO remove
64
65newtype PieceIndex = PieceIndex Int
66
67-- | An int used to denote piece size.
68newtype PieceSize = PieceSize Int
69 deriving (Show, Read, Typeable
70 , Eq, Ord, Enum
71 , Num, Real, Integral
72 , BEncode, ToJSON, FromJSON
73 )
74
75-- | Widely used semi-official block size.
76defaultBlockSize :: Int
77defaultBlockSize = 16 * 1024
78
79maxPieceSize :: Int
80maxPieceSize = 4 * 1024 * 1024
81{-# INLINE maxPieceSize #-}
82
83minPieceSize :: Int
84minPieceSize = defaultBlockSize * 4
85{-# INLINE minPieceSize #-}
86
87-- | NOTE: Have max and min size constrained to wide used
88-- semi-standard values. This bounds should be used to make decision
89-- about piece size for new torrents.
90--
91instance Bounded PieceSize where
92 maxBound = PieceSize maxPieceSize
93 {-# INLINE maxBound #-}
94
95 minBound = PieceSize minPieceSize
96 {-# INLINE minBound #-}
97
98-- | TODO
99optimalPieceCount :: Int
100optimalPieceCount = 1000
101{-# INLINE optimalPieceCount #-}
102
103toPow2 :: Int -> Int
104toPow2 x = bit $ fromIntegral (leadingZeros (0 :: Int) - leadingZeros x)
105
106-- | Find the optimal piece size for a given torrent size.
107defaultPieceSize :: Int64 -> Int
108defaultPieceSize x = max minPieceSize $ min maxPieceSize $ toPow2 pc
109 where
110 pc = fromIntegral (x `div` fromIntegral optimalPieceCount)
111
112-- TODO check if pieceLength is power of 2
113-- | Piece payload should be strict or lazy bytestring.
114data Piece a = Piece
115 { -- | Zero-based piece index in torrent. TODO how pieces are indexed?
116 pieceIndex :: {-# UNPACK #-} !PieceIx
117 -- | Payload.
118 , pieceData :: !a
119 } deriving (Show, Read, Eq, Typeable)
120
121$(deriveJSON (L.map toLower . L.dropWhile isLower) ''Piece)
122
123instance NFData (Piece a)
124
125-- | Format piece in human readable form. Payload bytes are omitted.
126ppPiece :: Piece a -> Doc
127ppPiece Piece {..}
128 = "Piece" <+> braces ("index" <+> "=" <+> int pieceIndex)
129
130data PieceInfo = PieceInfo
131 { piPieceLength :: {-# UNPACK #-} !PieceSize
132 -- ^ Number of bytes in each piece.
133
134 , piPieceHashes :: !ByteString
135 -- ^ Concatenation of all 20-byte SHA1 hash values.
136 } deriving (Show, Read, Eq, Typeable)
137
138$(deriveJSON (L.map toLower . L.dropWhile isLower) ''PieceInfo)
139
140-- | Number of bytes in each piece.
141makeLensesFor [("piPieceLength", "pieceLength")] ''PieceInfo
142
143-- | Concatenation of all 20-byte SHA1 hash values.
144makeLensesFor [("piPieceHashes", "pieceHashes")] ''PieceInfo
145
146instance NFData PieceInfo
147
148instance Lint PieceInfo where
149 lint pinfo @ PieceInfo {..}
150 | BS.length piPieceHashes `rem` hashsize == 0
151 , piPieceLength >= 0 = return pinfo
152 | otherwise = Left undefined
153
154
155putPieceInfo :: PieceInfo -> BDict -> BDict
156putPieceInfo PieceInfo {..} cont =
157 "piece length" .=! piPieceLength
158 .: "pieces" .=! piPieceHashes
159 .: cont
160
161getPieceInfo :: Get PieceInfo
162getPieceInfo = do
163 PieceInfo <$>! "piece length"
164 <*>! "pieces"
165
166instance BEncode PieceInfo where
167 toBEncode = toDict . (`putPieceInfo` endDict)
168 fromBEncode = fromDict getPieceInfo
169
170-- | Format piece info in human readable form. Hashes are omitted.
171ppPieceInfo :: PieceInfo -> Doc
172ppPieceInfo PieceInfo { piPieceLength = PieceSize len } =
173 "PieceInfo" <+> braces ("length" <+> "=" <+> int len)
174
175hashsize :: Int
176hashsize = 20
177{-# INLINE hashsize #-}
178
179slice :: Int -> Int -> ByteString -> ByteString
180slice start len = BS.take len . BS.drop start
181{-# INLINE slice #-}
182
183-- | Extract validation hash by specified piece index.
184pieceHash :: PieceInfo -> PieceIx -> ByteString
185pieceHash PieceInfo {..} i = slice (hashsize * i) hashsize piPieceHashes
186
187-- | Find count of pieces in the torrent. If torrent size is not a
188-- multiple of piece size then the count is rounded up.
189pieceCount :: PieceInfo -> PieceCount
190pieceCount PieceInfo {..} = BS.length piPieceHashes `quot` hashsize
191
192isLastPiece :: PieceInfo -> PieceIx -> Bool
193isLastPiece ci i = pieceCount ci == succ i
194
195class Validation a where
196 validate :: PieceInfo -> Piece a -> Bool
197
198-- | Validate piece with metainfo hash.
199checkPieceLazy :: PieceInfo -> Piece BL.ByteString -> Bool
200checkPieceLazy pinfo @ PieceInfo {..} Piece {..}
201 = (fromIntegral (BL.length pieceData) == piPieceLength
202 || isLastPiece pinfo pieceIndex)
203 && SHA1.hashlazy pieceData == pieceHash pinfo pieceIndex
diff --git a/src/Data/Torrent/Tree.hs b/src/Data/Torrent/Tree.hs
new file mode 100644
index 00000000..e9a337a1
--- /dev/null
+++ b/src/Data/Torrent/Tree.hs
@@ -0,0 +1,71 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8{-# LANGUAGE FlexibleInstances #-}
9{-# LANGUAGE TemplateHaskell #-}
10{-# LANGUAGE DeriveDataTypeable #-}
11module Data.Torrent.Tree
12 ( DirTree (..)
13 , build
14
15 , Data.Torrent.Tree.lookup
16 , lookupDir
17
18 , fileCount
19 , dirCount
20 ) where
21
22import Control.Arrow
23import Data.ByteString as BS
24import Data.ByteString.Char8 as BC
25import Data.Foldable
26import Data.List as L
27import Data.Map as M
28import Data.Monoid
29
30import Data.Torrent.Layout
31
32
33data DirTree a = Dir { children :: Map ByteString (DirTree a) }
34 | File { node :: FileInfo a }
35 deriving Show
36
37build :: LayoutInfo -> DirTree ()
38build SingleFile {liFile = FileInfo {..}} = Dir
39 { children = M.singleton fiName (File fi) }
40 where
41 fi = FileInfo fiLength fiMD5Sum ()
42build MultiFile {..} = Dir $ M.singleton liDirName files
43 where
44 files = Dir $ M.fromList $ L.map mkFileEntry liFiles
45 mkFileEntry FileInfo {..} = (L.head fiName, ent) -- TODO FIXME
46 where
47 ent = File $ FileInfo fiLength fiMD5Sum ()
48
49decompress :: DirTree () -> [FileInfo ()]
50decompress = undefined
51
52lookup :: [FilePath] -> DirTree a -> Maybe (DirTree a)
53lookup [] t = Just t
54lookup (p : ps) (Dir m) | Just subTree <- M.lookup (BC.pack p) m
55 = Data.Torrent.Tree.lookup ps subTree
56lookup _ _ = Nothing
57
58lookupDir :: [FilePath] -> DirTree a -> Maybe [(ByteString, DirTree a)]
59lookupDir ps d
60 | Just subTree <- Data.Torrent.Tree.lookup ps d =
61 case subTree of
62 File _ -> Nothing
63 Dir es -> Just $ M.toList es
64
65fileCount :: DirTree a -> Sum Int
66fileCount File {..} = Sum 1
67fileCount Dir {..} = foldMap fileCount children
68
69dirCount :: DirTree a -> Sum Int
70dirCount File {..} = Sum 0
71dirCount Dir {..} = Sum 1 <> foldMap dirCount children
diff --git a/src/Network/BitTorrent.hs b/src/Network/BitTorrent.hs
index 7ff85b39..e68d1597 100644
--- a/src/Network/BitTorrent.hs
+++ b/src/Network/BitTorrent.hs
@@ -7,7 +7,7 @@
7-- 7--
8{-# LANGUAGE RecordWildCards #-} 8{-# LANGUAGE RecordWildCards #-}
9module Network.BitTorrent 9module Network.BitTorrent
10 ( module Data.Torrent.Metainfo 10 ( module Data.Torrent
11 11
12 , TorrentLoc(..), TorrentMap, Progress(..) 12 , TorrentLoc(..), TorrentMap, Progress(..)
13 , ThreadCount, SessionCount 13 , ThreadCount, SessionCount
@@ -49,7 +49,7 @@ import Text.PrettyPrint
49import System.Directory 49import System.Directory
50import System.FilePath 50import System.FilePath
51 51
52import Data.Torrent.Metainfo 52import Data.Torrent
53import Network.BitTorrent.Sessions.Types 53import Network.BitTorrent.Sessions.Types
54import Network.BitTorrent.Sessions 54import Network.BitTorrent.Sessions
55import Network.BitTorrent.Extension 55import Network.BitTorrent.Extension
diff --git a/src/Network/BitTorrent/DHT/Protocol.hs b/src/Network/BitTorrent/DHT/Protocol.hs
index 0ac814b7..8528f0e0 100644
--- a/src/Network/BitTorrent/DHT/Protocol.hs
+++ b/src/Network/BitTorrent/DHT/Protocol.hs
@@ -29,10 +29,9 @@ import Network
29import Network.Socket 29import Network.Socket
30import System.Entropy 30import System.Entropy
31 31
32import Remote.KRPC
33import Remote.KRPC.Protocol
34import Data.BEncode 32import Data.BEncode
35import Data.Torrent.Metainfo 33import Network.KRPC
34import Network.KRPC.Protocol
36import Network.BitTorrent.Peer 35import Network.BitTorrent.Peer
37import Network.BitTorrent.Exchange.Protocol () 36import Network.BitTorrent.Exchange.Protocol ()
38 37
diff --git a/src/Network/BitTorrent/Exchange/Protocol.hs b/src/Network/BitTorrent/Exchange/Protocol.hs
index 8286ae7d..b86683fa 100644
--- a/src/Network/BitTorrent/Exchange/Protocol.hs
+++ b/src/Network/BitTorrent/Exchange/Protocol.hs
@@ -80,7 +80,7 @@ import Network.Socket.ByteString
80 80
81import Data.Torrent.Bitfield 81import Data.Torrent.Bitfield
82import Data.Torrent.Block 82import Data.Torrent.Block
83import Data.Torrent.Metainfo 83import Data.Torrent
84import Network.BitTorrent.Extension 84import Network.BitTorrent.Extension
85import Network.BitTorrent.Peer 85import Network.BitTorrent.Peer
86 86
diff --git a/src/Network/BitTorrent/Sessions.hs b/src/Network/BitTorrent/Sessions.hs
index 0f0d7ecd..a591c648 100644
--- a/src/Network/BitTorrent/Sessions.hs
+++ b/src/Network/BitTorrent/Sessions.hs
@@ -75,7 +75,7 @@ import Network.BSD
75import Network.Socket 75import Network.Socket
76 76
77import Data.Torrent.Bitfield as BF 77import Data.Torrent.Bitfield as BF
78import Data.Torrent.Metainfo 78import Data.Torrent
79import Network.BitTorrent.Extension 79import Network.BitTorrent.Extension
80import Network.BitTorrent.Peer 80import Network.BitTorrent.Peer
81import Network.BitTorrent.Sessions.Types 81import Network.BitTorrent.Sessions.Types
diff --git a/src/Network/BitTorrent/Sessions/Types.lhs b/src/Network/BitTorrent/Sessions/Types.lhs
index 7ee7cbec..6957032f 100644
--- a/src/Network/BitTorrent/Sessions/Types.lhs
+++ b/src/Network/BitTorrent/Sessions/Types.lhs
@@ -56,7 +56,6 @@
56> import Network 56> import Network
57 57
58> import Data.Torrent.Bitfield as BF 58> import Data.Torrent.Bitfield as BF
59> import Data.Torrent.Metainfo
60> import Network.BitTorrent.Extension 59> import Network.BitTorrent.Extension
61> import Network.BitTorrent.Peer 60> import Network.BitTorrent.Peer
62> import Network.BitTorrent.Exchange.Protocol as BT 61> import Network.BitTorrent.Exchange.Protocol as BT
diff --git a/src/Network/BitTorrent/Tracker.hs b/src/Network/BitTorrent/Tracker.hs
index 147d1ea5..c1dfe44e 100644
--- a/src/Network/BitTorrent/Tracker.hs
+++ b/src/Network/BitTorrent/Tracker.hs
@@ -36,7 +36,7 @@ import Data.Text as T
36import Network 36import Network
37import Network.URI 37import Network.URI
38 38
39import Data.Torrent.Metainfo 39import Data.Torrent
40import Network.BitTorrent.Peer 40import Network.BitTorrent.Peer
41import Network.BitTorrent.Tracker.Protocol as Tracker 41import Network.BitTorrent.Tracker.Protocol as Tracker
42import Network.BitTorrent.Tracker.HTTP 42import Network.BitTorrent.Tracker.HTTP
diff --git a/src/Network/BitTorrent/Tracker/HTTP.hs b/src/Network/BitTorrent/Tracker/HTTP.hs
index ce517b34..797115ad 100644
--- a/src/Network/BitTorrent/Tracker/HTTP.hs
+++ b/src/Network/BitTorrent/Tracker/HTTP.hs
@@ -30,7 +30,6 @@ import Data.URLEncoded as URL
30import Network.URI 30import Network.URI
31import Network.HTTP 31import Network.HTTP
32 32
33import Data.Torrent.Metainfo hiding (announce)
34import Network.BitTorrent.Tracker.Protocol 33import Network.BitTorrent.Tracker.Protocol
35 34
36 35
diff --git a/src/Network/BitTorrent/Tracker/Protocol.hs b/src/Network/BitTorrent/Tracker/Protocol.hs
index 965f3480..52835abb 100644
--- a/src/Network/BitTorrent/Tracker/Protocol.hs
+++ b/src/Network/BitTorrent/Tracker/Protocol.hs
@@ -58,7 +58,7 @@ import Data.Text (Text)
58import Data.Text.Encoding 58import Data.Text.Encoding
59import Data.Serialize hiding (Result) 59import Data.Serialize hiding (Result)
60import Data.URLEncoded as URL 60import Data.URLEncoded as URL
61import Data.Torrent.Metainfo 61import Data.Torrent
62import Network 62import Network
63import Network.URI 63import Network.URI
64import Network.Socket 64import Network.Socket
diff --git a/src/Network/BitTorrent/Tracker/UDP.hs b/src/Network/BitTorrent/Tracker/UDP.hs
index dc1b4897..f0bd3c56 100644
--- a/src/Network/BitTorrent/Tracker/UDP.hs
+++ b/src/Network/BitTorrent/Tracker/UDP.hs
@@ -42,7 +42,6 @@ import System.Entropy
42import System.Timeout 42import System.Timeout
43import Numeric 43import Numeric
44 44
45import Data.Torrent.Metainfo ()
46import Network.BitTorrent.Tracker.Protocol 45import Network.BitTorrent.Tracker.Protocol
47 46
48{----------------------------------------------------------------------- 47{-----------------------------------------------------------------------
diff --git a/src/System/Torrent/Storage.hs b/src/System/Torrent/Storage.hs
index 16f888bf..2225b0a3 100644
--- a/src/System/Torrent/Storage.hs
+++ b/src/System/Torrent/Storage.hs
@@ -50,7 +50,7 @@ import Foreign.C.Error
50 50
51import Data.Torrent.Bitfield as BF 51import Data.Torrent.Bitfield as BF
52import Data.Torrent.Block 52import Data.Torrent.Block
53import Data.Torrent.Metainfo 53import Data.Torrent
54import System.IO.MMap.Fixed as Fixed 54import System.IO.MMap.Fixed as Fixed
55 55
56-- TODO merge piece validation and Sessions.available into one transaction. 56-- TODO merge piece validation and Sessions.available into one transaction.
diff --git a/sub/bencoding b/sub/bencoding
Subproject 35913c887d8ef8f4e42398755bafb1439cd7c1a Subproject 3b7b6423103bba0a432f4348ea9af8042a11d48
diff --git a/tests/Data/Torrent/InfoHashSpec.hs b/tests/Data/Torrent/InfoHashSpec.hs
new file mode 100644
index 00000000..ba9ce9a4
--- /dev/null
+++ b/tests/Data/Torrent/InfoHashSpec.hs
@@ -0,0 +1,36 @@
1{-# OPTIONS -fno-warn-orphans #-}
2module Data.Torrent.InfoHashSpec (spec) where
3
4import Control.Applicative
5import System.FilePath
6import Test.Hspec
7import Test.QuickCheck
8import Test.QuickCheck.Instances ()
9
10import Data.Torrent
11import Data.Torrent.InfoHash as IH
12
13
14instance Arbitrary InfoHash where
15 arbitrary = IH.hash <$> arbitrary
16
17type TestPair = (FilePath, String)
18
19-- TODO add a few more torrents here
20torrentList :: [TestPair]
21torrentList =
22 [ ( "res" </> "dapper-dvd-amd64.iso.torrent"
23 , "0221caf96aa3cb94f0f58d458e78b0fc344ad8bf")
24 ]
25
26infohashSpec :: (FilePath, String) -> Spec
27infohashSpec (filepath, expectedHash) = do
28 it ("should match " ++ filepath) $ do
29 torrent <- fromFile filepath
30 let actualHash = show $ idInfoHash $ tInfoDict torrent
31 actualHash `shouldBe` expectedHash
32
33spec :: Spec
34spec = do
35 describe "info hash" $ do
36 mapM_ infohashSpec torrentList
diff --git a/tests/Data/Torrent/MagnetSpec.hs b/tests/Data/Torrent/MagnetSpec.hs
new file mode 100644
index 00000000..5adc6df7
--- /dev/null
+++ b/tests/Data/Torrent/MagnetSpec.hs
@@ -0,0 +1,44 @@
1{-# OPTIONS -fno-warn-orphans #-}
2module Data.Torrent.MagnetSpec (spec) where
3
4import Control.Applicative
5import Data.Maybe
6import Test.Hspec
7import Test.QuickCheck
8import Test.QuickCheck.Instances ()
9import Network.URI
10
11import Data.Torrent.InfoHash
12import Data.Torrent.Magnet
13import Data.Torrent.InfoHashSpec ()
14
15
16instance Arbitrary URIAuth where
17 arbitrary = URIAuth <$> arbitrary <*> arbitrary <*> arbitrary
18
19instance Arbitrary URI where
20 arbitrary
21 = pure $ fromJust $ parseURI "http://ietf.org/1737.txt?a=1&b=h#123"
22
23instance Arbitrary Magnet where
24 arbitrary = Magnet <$> arbitrary <*> arbitrary
25 <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
26 <*> arbitrary <*> arbitrary <*> pure (error "arbitrary magnet")
27
28magnetEncoding :: Magnet -> Bool
29magnetEncoding m = parseMagnet (renderMagnet m) == Just m
30
31spec :: Spec
32spec = do
33 describe "Magnet" $ do
34 it "properly encoded" $ property $ magnetEncoding
35
36 it "parse base32" $ do
37 let magnet = "magnet:?xt=urn:btih:CT76LXJDDCH5LS2TUHKH6EUJ3NYKX4Y6"
38 let ih = InfoHash "\DC4\255\229\221#\CAN\143\213\203S\161\212\DEL\DC2\137\219p\171\243\RS"
39 parseMagnet magnet `shouldBe` Just (nullMagnet ih)
40
41 it "parse base16" $ do
42 let magnet = "magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef01234567"
43 let ih = InfoHash "\SOH#Eg\137\171\205\239\SOH#Eg\137\171\205\239\SOH#Eg"
44 parseMagnet magnet `shouldBe` Just (nullMagnet ih)
diff --git a/tests/Data/Torrent/MetainfoSpec.hs b/tests/Data/Torrent/MetainfoSpec.hs
new file mode 100644
index 00000000..297b28f1
--- /dev/null
+++ b/tests/Data/Torrent/MetainfoSpec.hs
@@ -0,0 +1,76 @@
1{-# LANGUAGE TypeSynonymInstances #-}
2{-# OPTIONS -fno-warn-orphans #-}
3module Data.Torrent.MetainfoSpec (spec) where
4
5import Control.Applicative
6import Data.ByteString as BS
7import Data.ByteString.Lazy as BL
8import Data.BEncode
9import Data.Maybe
10import Network.URI
11import Test.Hspec
12import Test.QuickCheck
13import Test.QuickCheck.Instances ()
14
15import Data.Torrent.Layout
16import Data.Torrent
17
18
19{-----------------------------------------------------------------------
20-- Common
21-----------------------------------------------------------------------}
22
23data T a = T
24
25prop_properBEncode :: Show a => BEncode a => Eq a
26 => T a -> a -> Bool
27prop_properBEncode _ expected = actual == Right expected
28 where
29 actual = decode $ BL.toStrict $ encode expected
30
31instance Arbitrary URI where
32 arbitrary = pure $ fromJust
33 $ parseURI "http://exsample.com:80/123365_asd"
34
35{-----------------------------------------------------------------------
36-- Instances
37-----------------------------------------------------------------------}
38
39instance Arbitrary FileSize where
40 arbitrary = fromIntegral <$> (arbitrary :: Gen Int)
41
42instance Arbitrary a => Arbitrary (FileInfo a) where
43 arbitrary = FileInfo <$> arbitrary <*> arbitrary <*> arbitrary
44
45instance Arbitrary LayoutInfo where
46 arbitrary = oneof
47 [ SingleFile <$> arbitrary
48 , MultiFile <$> arbitrary <*> arbitrary
49 ]
50
51instance Arbitrary InfoDict where
52 arbitrary = undefined
53
54instance Arbitrary Torrent where
55 arbitrary = Torrent <$> arbitrary
56 <*> arbitrary <*> arbitrary <*> arbitrary
57 <*> arbitrary <*> arbitrary <*> arbitrary
58 <*> arbitrary <*> pure Nothing <*> arbitrary
59
60{-----------------------------------------------------------------------
61-- Spec
62-----------------------------------------------------------------------}
63
64spec :: Spec
65spec = do
66 describe "FileInfo" $ do
67 it "properly bencoded" $ property $
68 prop_properBEncode (T :: T (FileInfo BS.ByteString))
69
70 describe "LayoutInfo" $ do
71 it "properly bencoded" $ property $
72 prop_properBEncode (T :: T LayoutInfo)
73
74 describe "Torrent" $ do
75 it "property bencoded" $ property $
76 prop_properBEncode (T :: T Torrent)
diff --git a/tests/Spec.hs b/tests/Spec.hs
new file mode 100644
index 00000000..52ef578f
--- /dev/null
+++ b/tests/Spec.hs
@@ -0,0 +1 @@
{-# OPTIONS_GHC -F -pgmF hspec-discover #-} \ No newline at end of file