├── .gitignore ├── .travis.yml ├── COPYING ├── README.md ├── project.clj ├── resources └── fixtures │ ├── eceae16f50b11d3a6542b86aefdc1c6cb28ad708.torrent │ ├── multi-file.torrent │ ├── pictures │ ├── arches-9.jpg │ ├── architecture.jpg │ └── beach.jpg │ ├── private.torrent │ └── single-file.torrent └── src ├── main ├── clojure │ └── bencode │ │ ├── core.clj │ │ ├── metainfo │ │ ├── reader.clj │ │ └── writer.clj │ │ ├── protocol.clj │ │ ├── type │ │ ├── dict.clj │ │ ├── list.clj │ │ ├── number.clj │ │ ├── stream.clj │ │ └── string.clj │ │ └── utils.clj └── java │ └── bencode │ ├── FileSet.java │ ├── ParallelPieceDigest.java │ └── PieceDigestJob.java └── test └── clojure └── bencode ├── core_test.clj ├── java ├── file_set_test.clj ├── parallel_piece_digest_test.clj └── piece_digest_job_test.clj ├── metainfo ├── reader_test.clj └── writer_test.clj ├── type ├── dict_test.clj ├── list_test.clj ├── number_test.clj ├── string_test.clj └── unsupported_test.clj └── utils_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | pom.xml 6 | *.jar 7 | *.class 8 | .lein-deps-sum 9 | .lein-failures 10 | .lein-plugins 11 | .lein-repl-history 12 | *.asc -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Daniel Fernandes Martins 2 | 3 | http://github.com/danielfm/bencode 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of danielfm/bencode nor the names of its contributors 16 | may be used to endorse or promote products derived from this software 17 | without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 23 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bencode 2 | 3 | [![Build Status](https://travis-ci.org/danielfm/bencode.svg?branch=master)](https://travis-ci.org/danielfm/bencode) 4 | 5 | Clojure implementation of [Bencode](http://bittorrent.org/beps/bep_0003.html#bencoding), 6 | the encoding used by BitTorrent for storing and transmitting loosely structured data. 7 | 8 | ## Features 9 | 10 | * Parsing bencode strings directly to Clojure data structures and vice-versa 11 | * Support for input and output streams 12 | * Read and write BitTorrent metainfo (.torrent) files 13 | * Can generate Magnet link from BitTorrent metainfo 14 | * Multi-threaded algorithm for fast piece hashing 15 | 16 | 17 | ## Installation 18 | 19 | Add the following dependency to your _project.clj_ file: 20 | 21 | ````clojure 22 | 23 | [bencode "0.2.6"] 24 | ```` 25 | 26 | 27 | ## Usage 28 | 29 | ### Encoding and Decoding 30 | 31 | First, import the `bencode.core` namespace: 32 | 33 | ````clojure 34 | 35 | (use '[bencode.core]) 36 | ```` 37 | 38 | At this point, you should be able to use `bencode` and `bdecode` functions for 39 | encoding and decoding, respectively: 40 | 41 | ```clojure 42 | 43 | (bencode {:cow "moo" :spam ["info" 32]}) 44 | ;; -> "d3:cow3:moo4:spaml4:infoi32eee" 45 | 46 | (bdecode "d3:cow3:moo4:spaml4:infoi32eee") 47 | ;; -> {:cow "moo", :spam ["info" 32]} 48 | ``` 49 | 50 | 51 | ##### Supported Data Types 52 | 53 | According to the Bencoding spec, only _strings_, _integers_, _lists_ and 54 | _dictionaries_ should be supported. Furthermore, only strings can be used as 55 | keys in a dictionary, and the keys must appear in sorted order (sorted as raw 56 | strings, not alphanumerics). 57 | 58 | On the Clojure side, _keywords_ are encoded as _strings_, _sets_ and _vectors_ 59 | are encoded as _lists_, and all integers - _byte_, _short_, _int_, _long_, 60 | _big integers_ - are encoded as _integers_ with arbitrary size, and decoded to 61 | the smallest type which can hold the number without losing data. 62 | 63 | 64 | #### Encoding Options 65 | 66 | The `bencode` function also accepts an optional map: 67 | 68 | ````clojure 69 | 70 | (bencode "moo" {:raw-str? true}) 71 | ;; -> # 72 | ```` 73 | 74 | These are the supported encoding options: 75 | 76 | * `:to` - Instance of `OutputStream` where the encoding result should be 77 | written to. Default: `nil` 78 | * `:raw-str?` - Whether the string being encoded should be returned as a 79 | byte array. This option can only be used if the option `:to` is absent. 80 | Default: `false` 81 | 82 | 83 | #### Decoding Options 84 | 85 | The `bdecode` function also accepts an optional map: 86 | 87 | ````clojure 88 | 89 | (bdecode "d3:cow3:moo4:spaml4:infoi32eee", {:str-keys? true :raw-keys ["spam"]}) 90 | ;; -> {"cow" "moo", "spam" [# 32]} 91 | ```` 92 | 93 | The input might be either a _string_, a _byte array_ or an _input stream_. 94 | 95 | These are the supported decoding options: 96 | 97 | * `:str-keys?` - Whether strings should be used as dictionary keys instead of 98 | keywords. Default: `false` 99 | * `:raw-keys` - List containing all dictionary keys whose values should be 100 | decoded as raw strings instead of UTF-8-encoded strings. Default: `nil` 101 | 102 | 103 | ### BitTorrent Metainfo 104 | 105 | #### Reading Metainfo Files 106 | 107 | A collection of useful functions for BitTorrent metainfo parsing are available 108 | under the `bencode.metainfo.reader` namespace: 109 | 110 | ````clojure 111 | 112 | (use '[bencode.metainfo.reader]) 113 | 114 | ;; parsing an input stream 115 | (def metainfo (parse-metainfo input-stream)) 116 | 117 | ;; parsing a file given its path 118 | (def metainfo (parse-metainfo-file "/file/path")) 119 | ```` 120 | 121 | To extract bits of information from this metainfo: 122 | 123 | ````clojure 124 | 125 | (torrent-name metainfo) 126 | ;; -> "my.supercool.torrent" 127 | 128 | (public-torrent? metainfo) 129 | ;; -> true 130 | 131 | (torrent-info-hash-str metainfo) 132 | ;; -> "b174c9c090275f858853ba5ea1b01762eaa59f9d" 133 | ```` 134 | 135 | It's also possible to generate the Magnet link for a metainfo: 136 | 137 | ````clojure 138 | 139 | (torrent-magnet-link metainfo) 140 | ;; -> "magnet:?xt=urn:btih:..." 141 | ```` 142 | 143 | Please check out the source code for a complete list of the available functions. 144 | 145 | #### Creating a Metainfo Dictionary 146 | 147 | It's very easy to create a metainfo file: 148 | 149 | ````clojure 150 | 151 | (use '[bencode.metainfo.writer]) 152 | 153 | (def metainfo (create-metainfo :file file-obj 154 | :created-by "You" 155 | :announce-list [["tracker-1"] ["tracker-2"]] 156 | :private? false 157 | :name "optional.torrent.name" 158 | :piece-length-power 7 ;; piece length = 2^7KiB 159 | :n-threads 4 ;; for fast parallel piece hashing 160 | :comment "Some torrent")) 161 | ```` 162 | 163 | This operation might take several minutes depending on the file size. 164 | 165 | To be able to import this file in your favorite BitTorrent client, just bencode 166 | `metainfo` to a _.torrent_ file and you're done: 167 | 168 | 169 | ````clojure 170 | 171 | (bencode metainfo {:to file-out-stream}) 172 | ```` 173 | 174 | ## Donate 175 | 176 | If this project is useful for you, buy me a beer! 177 | 178 | Bitcoin: `bc1qtwyfcj7pssk0krn5wyfaca47caar6nk9yyc4mu` 179 | 180 | ## License 181 | 182 | Copyright (C) Daniel Fernandes Martins 183 | 184 | Distributed under the New BSD License. See COPYING for further details. 185 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject bencode "0.2.6" 2 | :description "BitTorrent encoding implementation for Clojure." 3 | :url "https://github.com/danielfm/bencode" 4 | :license {:name "BSD License" 5 | :url "http://raw.github.com/danielfm/bencode/master/COPYING"} 6 | :dependencies [[org.clojure/clojure "1.5.1"] 7 | [commons-codec "1.8"]] 8 | :source-paths ["src/main/clojure"] 9 | :java-source-paths ["src/main/java"] 10 | :test-paths ["src/test/clojure"] 11 | :jar-exclusions [#"fixtures/"]) 12 | -------------------------------------------------------------------------------- /resources/fixtures/eceae16f50b11d3a6542b86aefdc1c6cb28ad708.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielfm/bencode/4b47e1c94394c1b5b74171f053873a5df772a8c7/resources/fixtures/eceae16f50b11d3a6542b86aefdc1c6cb28ad708.torrent -------------------------------------------------------------------------------- /resources/fixtures/multi-file.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielfm/bencode/4b47e1c94394c1b5b74171f053873a5df772a8c7/resources/fixtures/multi-file.torrent -------------------------------------------------------------------------------- /resources/fixtures/pictures/arches-9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielfm/bencode/4b47e1c94394c1b5b74171f053873a5df772a8c7/resources/fixtures/pictures/arches-9.jpg -------------------------------------------------------------------------------- /resources/fixtures/pictures/architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielfm/bencode/4b47e1c94394c1b5b74171f053873a5df772a8c7/resources/fixtures/pictures/architecture.jpg -------------------------------------------------------------------------------- /resources/fixtures/pictures/beach.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielfm/bencode/4b47e1c94394c1b5b74171f053873a5df772a8c7/resources/fixtures/pictures/beach.jpg -------------------------------------------------------------------------------- /resources/fixtures/private.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielfm/bencode/4b47e1c94394c1b5b74171f053873a5df772a8c7/resources/fixtures/private.torrent -------------------------------------------------------------------------------- /resources/fixtures/single-file.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielfm/bencode/4b47e1c94394c1b5b74171f053873a5df772a8c7/resources/fixtures/single-file.torrent -------------------------------------------------------------------------------- /src/main/clojure/bencode/core.clj: -------------------------------------------------------------------------------- 1 | (ns bencode.core 2 | (:require [bencode.protocol :as protocol]) 3 | (:use [bencode.type.number] 4 | [bencode.type.string] 5 | [bencode.type.list] 6 | [bencode.type.dict] 7 | [bencode.type.stream]) 8 | (:import [java.io ByteArrayOutputStream])) 9 | 10 | (defn bencode 11 | "Bencodes the given object." 12 | ([obj] 13 | (bencode obj nil)) 14 | ([obj opts] 15 | (let [^ByteArrayOutputStream out (or (get opts :to (ByteArrayOutputStream.)))] 16 | (protocol/bencode! obj out opts) 17 | (when-not (:to opts) 18 | (let [^bytes arr (.toByteArray out)] 19 | (if (:raw-str? opts) 20 | arr 21 | (String. arr "UTF-8"))))))) 22 | 23 | (defn bdecode 24 | "Bdecodes the given input." 25 | ([in] 26 | (protocol/bdecode in nil)) 27 | ([in opts] 28 | (protocol/bdecode in opts))) 29 | -------------------------------------------------------------------------------- /src/main/clojure/bencode/metainfo/reader.clj: -------------------------------------------------------------------------------- 1 | (ns bencode.metainfo.reader 2 | (:use [bencode.core]) 3 | (:require [clojure.java.io :as io]) 4 | (:import [java.io File] 5 | [java.net URLEncoder] 6 | [java.security MessageDigest] 7 | [java.util Date] 8 | [org.apache.commons.codec.binary Base32])) 9 | 10 | (defn- hex-from-bytes 11 | "Converts a byte-array to a hex string." 12 | [byte-arr] 13 | (let [sb (StringBuffer.)] 14 | (doseq [b byte-arr] 15 | (.append sb (.substring (Integer/toString (+ (bit-and b 0xff) 0x100) 16) 1))) 16 | (.toString sb))) 17 | 18 | (defn parse-metainfo 19 | "Bdecodes the given torrent metainfo input." 20 | [in] 21 | (bdecode in {:str-keys? true 22 | :raw-keys ["pieces"]})) 23 | 24 | (defn parse-metainfo-file 25 | "Bdecodes the torrent metainfo located at file-path." 26 | [file-path] 27 | (with-open [in (io/input-stream file-path)] 28 | (parse-metainfo in))) 29 | 30 | (defn single-file-torrent? 31 | "Returns whether metainfo represents a single-file torrent." 32 | [metainfo] 33 | (pos? (get-in metainfo ["info" "length"] 0))) 34 | 35 | (defn multi-file-torrent? 36 | "Returns whether metainfo represents a multi-file torrent." 37 | [metainfo] 38 | (not (single-file-torrent? metainfo))) 39 | 40 | (defn private-torrent? 41 | "Returns whether metainfo represents a private torrent." 42 | [metainfo] 43 | (not= 0 (get-in metainfo ["info" "private"] 0))) 44 | 45 | (defn public-torrent? 46 | "Returns whether metainfo represents a public torrent." 47 | [metainfo] 48 | (not (private-torrent? metainfo))) 49 | 50 | (defn torrent-name 51 | "Returns the torrent name." 52 | [metainfo] 53 | (or (get-in metainfo ["info" "name.utf-8"]) 54 | (get-in metainfo ["info" "name"]))) 55 | 56 | (defn torrent-announce 57 | "Returns the torrent tracker URL." 58 | [metainfo] 59 | (get metainfo "announce")) 60 | 61 | (defn torrent-announce-list 62 | "Returns the list of torrent tracker URLs." 63 | [metainfo] 64 | (get metainfo "announce-list")) 65 | 66 | (defn torrent-piece-length 67 | "Returns the length of each piece, in bytes." 68 | [metainfo] 69 | (get-in metainfo ["info" "piece length"])) 70 | 71 | (defn torrent-pieces-hash 72 | "Returns the torrent pieces hash." 73 | [metainfo] 74 | (get-in metainfo ["info" "pieces"])) 75 | 76 | (defn torrent-pieces-hash-list 77 | "Returns a lazy seq containing the hash for each piece." 78 | [metainfo] 79 | (map hex-from-bytes 80 | (partition 20 (torrent-pieces-hash metainfo)))) 81 | 82 | (defn torrent-pieces-count 83 | "Returns the number of pieces." 84 | [metainfo] 85 | (/ (count (torrent-pieces-hash metainfo)) 20)) 86 | 87 | (defn torrent-size 88 | "Return the total size of the torrent, in bytes." 89 | [metainfo] 90 | (or (get-in metainfo ["info" "length"]) 91 | (reduce + (map #(get % "length") 92 | (get-in metainfo ["info" "files"]))))) 93 | 94 | (defn torrent-info-hash 95 | "Returns the byte array of the torrent info hash." 96 | [metainfo] 97 | (let [enc (bencode (get metainfo "info") {:raw-str? true}) 98 | dig (MessageDigest/getInstance "SHA1")] 99 | (.digest dig enc))) 100 | 101 | (defn torrent-info-hash-str 102 | "Returns the torrent info hash in a readable format." 103 | [metainfo] 104 | (hex-from-bytes (torrent-info-hash metainfo))) 105 | 106 | (defn torrent-creation-date 107 | "Returns an instance of java.util.Date representing the torrent 108 | creation date." 109 | [metainfo] 110 | (if-let [^int timestamp (get metainfo "creation date")] 111 | (Date. (* 1000 timestamp)))) 112 | 113 | (defn torrent-files 114 | "Returns the torrent list of files." 115 | [metainfo] 116 | (get-in metainfo ["info" "files"])) 117 | 118 | (defn torrent-created-by 119 | "Returns the torrent maker name." 120 | [metainfo] 121 | (get metainfo "created by")) 122 | 123 | (defn torrent-comment 124 | "Returns the torrent comment." 125 | [metainfo] 126 | (get metainfo "comment")) 127 | 128 | (defn torrent-magnet-link 129 | "Returns the torrent magnet link." 130 | [metainfo] 131 | (str "magnet:?xt=urn:btih:" (torrent-info-hash-str metainfo) 132 | "&dn=" (URLEncoder/encode (torrent-name metainfo) "UTF-8") 133 | (reduce #(str %1 "&tr=" (URLEncoder/encode %2 "UTF-8")) 134 | "" (flatten (torrent-announce-list metainfo))))) 135 | -------------------------------------------------------------------------------- /src/main/clojure/bencode/metainfo/writer.clj: -------------------------------------------------------------------------------- 1 | (ns bencode.metainfo.writer 2 | (:use [bencode.core] 3 | [bencode.utils]) 4 | (:require [clojure.string :as str] 5 | [clojure.java.io :as io]) 6 | (:import [java.io File] 7 | [java.nio ByteBuffer] 8 | [java.security MessageDigest] 9 | [java.util.concurrent TimeUnit])) 10 | 11 | (defn file-path 12 | "Returns the file path relative to directory dir." 13 | [^File dir ^File file] 14 | (let [sep File/separator 15 | dir-path (str (.getCanonicalPath dir) sep) 16 | file-path (.getCanonicalPath file)] 17 | (str/split (str/replace file-path dir-path "") (re-pattern sep)))) 18 | 19 | (defn file-entry 20 | "Returns a dictionary that represents a torrent file entry." 21 | [^File dir ^File file] 22 | {"length" (.length file) "path" (file-path dir file)}) 23 | 24 | (defn scan-files 25 | "Returns a seq containing all files inside directory dir." 26 | [^File dir] 27 | (filter #(.isFile ^File %) (file-seq dir))) 28 | 29 | (defn torrent-files 30 | "Returns a seq containing all file entries from the files inside dir." 31 | [^File dir files] 32 | (let [path (.getCanonicalPath dir)] 33 | (map #(file-entry dir %) files))) 34 | 35 | (defn torrent-piece-length 36 | "Returns the size, in bytes, for a piece length of 2^n KB." 37 | [n] 38 | (let [x (or n 8)] 39 | (int (* 1024 (Math/pow 2 x))))) 40 | 41 | (defn torrent-main-announce 42 | "Returns the first URL from the first announce group." 43 | [announce-list] 44 | (first (first announce-list))) 45 | 46 | (defn- assoc-pieces-hash 47 | "Computes the hash for each piece in parallel and appends them to metainfo 48 | dictionary." 49 | [files metainfo n-threads] 50 | (let [piece-length (get-in metainfo ["info" "piece length"]) 51 | digest (bencode.ParallelPieceDigest. files piece-length)] 52 | (assoc-in metainfo ["info" "pieces"] (.computeHash digest n-threads)))) 53 | 54 | (defn- single-file-metainfo 55 | "Returns a metainfo dictionary for single-file torrent." 56 | [^File file base-metainfo n-threads] 57 | (assoc-pieces-hash [file] 58 | (assoc-in base-metainfo ["info" "length"] 59 | (.length file)) 60 | n-threads)) 61 | 62 | (defn- multi-file-metainfo 63 | "Returns a metainfo dictionary for multi-file torrent." 64 | [^File dir files base-metainfo n-threads] 65 | (assoc-pieces-hash files 66 | (assoc-in base-metainfo ["info" "files"] 67 | (torrent-files dir files)) 68 | n-threads)) 69 | 70 | (defn create-metainfo 71 | "Creates a BitTorrent metainfo dictionary." 72 | [& {:keys [^File file announce-list name comment created-by 73 | piece-length-power private? n-threads]}] 74 | (let [now (System/currentTimeMillis) 75 | n-thread (or n-threads 2) 76 | files (scan-files file) 77 | created-at (.toSeconds TimeUnit/MILLISECONDS now) 78 | created-by (or created-by "Bencode") 79 | torrent-name (or name (.getName file)) 80 | private-flag (flag-from-bool private?) 81 | comment (or comment "") 82 | main-announce (torrent-main-announce announce-list) 83 | piece-length (torrent-piece-length piece-length-power) 84 | base-metainfo {"info" {"name" torrent-name 85 | "private" private-flag 86 | "piece length" piece-length} 87 | "created by" created-by 88 | "creation date" created-at 89 | "comment" comment 90 | "announce" main-announce 91 | "announce-list" announce-list}] 92 | (if (.isFile file) 93 | (single-file-metainfo file base-metainfo n-threads) 94 | (multi-file-metainfo file files base-metainfo n-threads)))) 95 | -------------------------------------------------------------------------------- /src/main/clojure/bencode/protocol.clj: -------------------------------------------------------------------------------- 1 | (ns bencode.protocol 2 | (:use [bencode.utils]) 3 | (:import [java.io InputStream])) 4 | 5 | (defprotocol Bdecodable 6 | "All bdecodable data types must implement this protocol." 7 | (bdecode [self opts] "Returns the data structure from this bencoded object.")) 8 | 9 | (defprotocol Bencodable 10 | "All bencodable data types must implement this protocol." 11 | (bencode! [self out opts] "Returns a bencoded string of self.")) 12 | 13 | (defmulti bdecode-type! 14 | "Dispatches to the proper decoder method according to the next byte read 15 | from input stream in." 16 | (fn [^InputStream in _] 17 | (.mark in 1) 18 | (let [b (.read in) 19 | ch (char b)] 20 | (cond 21 | (= ch \i) :number 22 | (= ch \l) :list 23 | (= ch \d) :dict 24 | (digit? b) (do (.reset in) :string) 25 | :else :unknown)))) 26 | 27 | (defmethod bdecode-type! :unknown [seq opts] 28 | (error "Unexpected token")) 29 | -------------------------------------------------------------------------------- /src/main/clojure/bencode/type/dict.clj: -------------------------------------------------------------------------------- 1 | (ns bencode.type.dict 2 | (:use [bencode.protocol] 3 | [bencode.utils]) 4 | (:import [java.util Map] 5 | [java.io InputStream OutputStream])) 6 | 7 | (defn- valid-dict? 8 | "Returns whether the dictionary m only contains valid keys." 9 | [m] 10 | (every? (some-fn keyword? string?) 11 | (keys m))) 12 | 13 | (defn- sort-dict 14 | "Returns a normalized and sorted version of dictionary m." 15 | [m] 16 | (into (sorted-map) 17 | (map (fn [[k v]] 18 | [(name k) v]) 19 | m))) 20 | 21 | (defn- dict-key 22 | "Returns the final dictionary key according to the options defined in opts." 23 | [key opts] 24 | (if (:str-keys? opts) 25 | key 26 | (keyword key))) 27 | 28 | (defn- bdecode-dict-entry! 29 | "Bdecodes a dictionary entry from input stream in, where the key and its 30 | corresponding value comes in a row." 31 | [in opts] 32 | (let [raw-keys (:raw-keys opts) 33 | key (bdecode-type! in opts) 34 | val (bdecode-type! in (if (some #{key} raw-keys) 35 | (assoc opts :raw-str? true) 36 | opts))] 37 | [key val])) 38 | 39 | (extend-protocol Bencodable 40 | Map 41 | (bencode! [self ^OutputStream out opts] 42 | (when-not (valid-dict? self) 43 | (error "Only keywords can be used as dictionary keys")) 44 | (.write out (int \d)) 45 | (doall 46 | (map (fn [[key val]] 47 | (bencode! key out opts) 48 | (bencode! val out opts)) 49 | (sort-dict self))) 50 | (.write out (int \e)))) 51 | 52 | (defmethod bdecode-type! :dict [^InputStream in opts] 53 | (loop [data (sorted-map)] 54 | (.mark in 1) 55 | (if (= \e (char (.read in))) 56 | data 57 | (do 58 | (.reset in) 59 | (let [[key val] (bdecode-dict-entry! in opts)] 60 | (if (string? key) 61 | (recur (assoc data (dict-key key opts) val)) 62 | (error "Only strings should be used as dictionary keys"))))))) 63 | -------------------------------------------------------------------------------- /src/main/clojure/bencode/type/list.clj: -------------------------------------------------------------------------------- 1 | (ns bencode.type.list 2 | (:use [bencode.protocol] 3 | [bencode.utils]) 4 | (:import [java.util Set] 5 | [java.io InputStream OutputStream])) 6 | 7 | (defn- bencode-seq! 8 | "Bencodes the given sequence." 9 | [seq ^OutputStream out opts] 10 | (.write out (int \l)) 11 | (doall (map #(bencode! % out opts) seq)) 12 | (.write out (int \e))) 13 | 14 | (extend-protocol Bencodable 15 | java.util.Set 16 | (bencode! [self out opts] 17 | (bencode-seq! self out opts)) 18 | 19 | java.util.List 20 | (bencode! [self out opts] 21 | (bencode-seq! self out opts))) 22 | 23 | (defmethod bdecode-type! :list [^InputStream in opts] 24 | (loop [data []] 25 | (.mark in 1) 26 | (if (= \e (char (.read in))) 27 | data 28 | (do 29 | (.reset in) 30 | (let [item (bdecode-type! in opts)] 31 | (recur (conj data item))))))) 32 | -------------------------------------------------------------------------------- /src/main/clojure/bencode/type/number.clj: -------------------------------------------------------------------------------- 1 | (ns bencode.type.number 2 | (:require [clojure.edn :as edn]) 3 | (:use [bencode.protocol] 4 | [bencode.utils]) 5 | (:import [java.io InputStream OutputStream])) 6 | 7 | (defn- bencode-number! 8 | "Bencodes the given number, which is assumed to be an integer." 9 | [^Number n ^OutputStream out] 10 | (let [^String s (str "i" n "e")] 11 | (.write out (.getBytes s) 0 (count s)))) 12 | 13 | (defn- invalid-number? 14 | "Returns whether the string of digits represents an invalid number according 15 | to the spec." 16 | [^String digits] 17 | (or (empty? digits) 18 | (and (> (count digits) 1) 19 | (= \0 (char (first digits)))))) 20 | 21 | (extend-protocol Bencodable 22 | Byte 23 | (bencode! [self out opts] 24 | (bencode-number! self out)) 25 | 26 | Short 27 | (bencode! [self out opts] 28 | (bencode-number! self out)) 29 | 30 | Integer 31 | (bencode! [self out opts] 32 | (bencode-number! self out)) 33 | 34 | Long 35 | (bencode! [self out opts] 36 | (bencode-number! self out)) 37 | 38 | BigInteger 39 | (bencode! [self out opts] 40 | (bencode-number! self out)) 41 | 42 | clojure.lang.BigInt 43 | (bencode! [self out opts] 44 | (bencode-number! self out))) 45 | 46 | (defmethod bdecode-type! :number [^InputStream in opts] 47 | (.mark in 1) 48 | (let [f (char (.read in)) 49 | sign (#{\- \+} f)] 50 | (when-not sign 51 | (.reset in)) 52 | (.mark in 1) 53 | (let [s (char (.read in))] 54 | (if (and (= \- sign) (= \0 s)) 55 | (error "Invalid number expression") 56 | (do 57 | (.reset in) 58 | (let [digits (read-digits! in)] 59 | (.reset in) 60 | (if (or (invalid-number? digits) 61 | (not (= \e (char (.read in))))) 62 | (error "Invalid number expression") 63 | (edn/read-string (str sign digits))))))))) 64 | -------------------------------------------------------------------------------- /src/main/clojure/bencode/type/stream.clj: -------------------------------------------------------------------------------- 1 | (ns bencode.type.stream 2 | (:use [bencode.protocol] 3 | [bencode.utils])) 4 | 5 | (extend-protocol Bdecodable 6 | java.io.InputStream 7 | (bdecode [self opts] 8 | (let [out (bdecode-type! self opts)] 9 | (if (= -1 (.read self)) 10 | out 11 | (error "Unexpected trailing data"))))) 12 | -------------------------------------------------------------------------------- /src/main/clojure/bencode/type/string.clj: -------------------------------------------------------------------------------- 1 | (ns bencode.type.string 2 | (:require [clojure.edn :as edn]) 3 | (:use [bencode.protocol] 4 | [bencode.utils]) 5 | (:import [java.io InputStream OutputStream])) 6 | 7 | (extend-protocol Bdecodable 8 | String 9 | (bdecode [self opts] 10 | (bdecode (.getBytes self "UTF-8") opts))) 11 | 12 | (extend-type (Class/forName "[B") 13 | Bdecodable 14 | (bdecode [self opts] 15 | (let [stream (java.io.ByteArrayInputStream. self)] 16 | (bdecode stream opts)))) 17 | 18 | (extend-protocol Bencodable 19 | String 20 | (bencode! [self out opts] 21 | (bencode! (.getBytes self "UTF-8") out opts)) 22 | 23 | clojure.lang.Keyword 24 | (bencode! [self out opts] 25 | (bencode! (name self) out opts))) 26 | 27 | (extend-type (Class/forName "[B") 28 | Bencodable 29 | (bencode! [^String self ^OutputStream out opts] 30 | (let [^String len (str (count self))] 31 | (.write out (.getBytes len) 0 (count len)) 32 | (.write out (int \:)) 33 | (.write out self 0 (count self))))) 34 | 35 | (defmethod bdecode-type! :string [^InputStream in opts] 36 | (let [^String size (read-digits! in) 37 | ^String len(edn/read-string size) 38 | ^bytes data (byte-array len)] 39 | (when-not (= \: (char (.read in))) 40 | (error "Expected ':'")) 41 | (if (and (> len 0) (< (.read in data 0 len) len)) 42 | (error "Unterminated string") 43 | (if (:raw-str? opts) 44 | data 45 | (String. data "UTF-8"))))) 46 | -------------------------------------------------------------------------------- /src/main/clojure/bencode/utils.clj: -------------------------------------------------------------------------------- 1 | (ns bencode.utils 2 | (:import [java.io InputStream])) 3 | 4 | (defn flag-from-bool 5 | "Returns 1 if b is true, false otherwise." 6 | [b] 7 | (if b 1 0)) 8 | 9 | (defn digit? 10 | "Returns whether byte b is a digit, e.g., value between 0 and 9." 11 | [b] 12 | (and (>= b 48) (<= b 57))) 13 | 14 | (defn read-digits! 15 | "Reads from input stream in until a non-digit char is found and returns 16 | a string containing the digits read." 17 | [^InputStream in] 18 | (loop [data []] 19 | (.mark in 1) 20 | (let [f (.read in)] 21 | (if (digit? f) 22 | (recur (conj data (char f))) 23 | (do 24 | (.reset in) 25 | (apply str data)))))) 26 | 27 | (defn error 28 | "Raises an exception that signals an unexpected condition during 29 | encoding/decoding process." 30 | [^String msg] 31 | (throw (IllegalArgumentException. msg))) 32 | -------------------------------------------------------------------------------- /src/main/java/bencode/FileSet.java: -------------------------------------------------------------------------------- 1 | package bencode; 2 | 3 | import java.util.List; 4 | import java.io.File; 5 | 6 | public class FileSet { 7 | private List files; 8 | 9 | public FileSet(List files) { 10 | this.files = files; 11 | } 12 | 13 | public List getFiles() { 14 | return files; 15 | } 16 | 17 | public int totalPieces(int pieceLength) { 18 | return (int) Math.ceil((double) this.totalSize() / pieceLength); 19 | } 20 | 21 | private long totalSize() { 22 | long size = 0L; 23 | 24 | for (File file : this.files) { 25 | size += file.length(); 26 | } 27 | 28 | return size; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/bencode/ParallelPieceDigest.java: -------------------------------------------------------------------------------- 1 | package bencode; 2 | 3 | import java.io.File; 4 | import java.io.FileInputStream; 5 | import java.io.IOException; 6 | 7 | import java.util.List; 8 | 9 | import java.util.concurrent.atomic.AtomicInteger; 10 | import java.util.concurrent.Executors; 11 | import java.util.concurrent.ExecutorService; 12 | 13 | public class ParallelPieceDigest { 14 | private final FileSet fileSet; 15 | private final int pieceLength; 16 | 17 | public ParallelPieceDigest(List files, int pieceLength) { 18 | this.fileSet = new FileSet(files); 19 | this.pieceLength = pieceLength; 20 | } 21 | 22 | public byte[] computeHash(int numWorkers) throws IOException { 23 | AtomicInteger hashedPieces = new AtomicInteger(); 24 | 25 | int piece = 0; 26 | int totalPieces = this.fileSet.totalPieces(this.pieceLength); 27 | 28 | byte[] globalHash = new byte[totalPieces * 20]; 29 | byte[] pieceBuffer = new byte[this.pieceLength]; 30 | 31 | int bytesRead = 0; 32 | int bytesLeft = this.pieceLength; 33 | 34 | FileInputStream in = null; 35 | 36 | ExecutorService pool = Executors.newFixedThreadPool(numWorkers); 37 | 38 | try { 39 | for (File file : this.fileSet.getFiles()) { 40 | in = new FileInputStream(file); 41 | int lastRead = 0; 42 | 43 | do { 44 | lastRead = in.read(pieceBuffer, bytesRead, bytesLeft); 45 | 46 | if (lastRead >= 0) { 47 | bytesRead += lastRead; 48 | bytesLeft -= lastRead; 49 | } 50 | 51 | // piece is ready for hashing 52 | if (bytesLeft == 0) { 53 | // new buffer for the next piece 54 | byte[] pieceData = pieceBuffer; 55 | pieceBuffer = new byte[this.pieceLength]; 56 | 57 | // submit piece for hashing 58 | pool.execute(new PieceDigestJob(globalHash, pieceData, bytesRead, piece++, hashedPieces)); 59 | 60 | // new piece 61 | bytesRead = 0; 62 | bytesLeft = this.pieceLength; 63 | } 64 | } while (lastRead > 0); 65 | 66 | // eof 67 | in.close(); 68 | } 69 | 70 | // submit the final piece for hashing 71 | pool.execute(new PieceDigestJob(globalHash, pieceBuffer, bytesRead, piece++, hashedPieces)); 72 | 73 | // wait for all workers to finish 74 | while (hashedPieces.get() < totalPieces); 75 | } 76 | finally { 77 | if (in != null) { 78 | in.close(); 79 | } 80 | 81 | pool.shutdown(); 82 | } 83 | 84 | return globalHash; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/bencode/PieceDigestJob.java: -------------------------------------------------------------------------------- 1 | package bencode; 2 | 3 | import java.util.concurrent.atomic.AtomicInteger; 4 | 5 | import java.security.MessageDigest; 6 | import java.security.NoSuchAlgorithmException; 7 | 8 | public class PieceDigestJob implements Runnable { 9 | private byte[] globalHash; 10 | private byte[] data; 11 | private int dataLength; 12 | private int piece; 13 | private AtomicInteger hashedPieces; 14 | 15 | public PieceDigestJob(byte[] globalHash, byte[] data, int dataLength, int piece, AtomicInteger hashedPieces) { 16 | this.globalHash = globalHash; 17 | this.data = data; 18 | this.dataLength = dataLength; 19 | this.piece = piece; 20 | this.hashedPieces = hashedPieces; 21 | } 22 | 23 | public void run() { 24 | try { 25 | MessageDigest sha = MessageDigest.getInstance("SHA1"); 26 | 27 | // computes the sha-1 hash for this piece 28 | sha.update(this.data, 0, this.dataLength); 29 | byte[] hash = sha.digest(); 30 | 31 | // updates the global hash 32 | for (int i = 0; i < hash.length; i++) { 33 | this.globalHash[20 * this.piece + i] = hash[i]; 34 | } 35 | 36 | // increment the number of hashed pieces until now 37 | this.hashedPieces.incrementAndGet(); 38 | 39 | // giving a hand to the GC 40 | this.globalHash = null; 41 | this.data = null; 42 | } 43 | catch (NoSuchAlgorithmException ex) { 44 | ex.printStackTrace(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/clojure/bencode/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns bencode.core-test 2 | (:use clojure.test 3 | bencode.core)) 4 | 5 | (deftest encoding-options 6 | (testing "Encoding output as string" 7 | (is (= "4:spam" (bencode "spam")))) 8 | 9 | (testing "Encoding output as a byte array" 10 | (is (= (vec (.getBytes "4:spam")) 11 | (vec (bencode "spam" {:raw-str? true}))))) 12 | 13 | (testing "Encoding output to a custom stream" 14 | (let [stream (java.io.ByteArrayOutputStream.)] 15 | (bencode "spam" {:to stream}) 16 | (is (= "4:spam" (String. (.toByteArray stream))))))) 17 | -------------------------------------------------------------------------------- /src/test/clojure/bencode/java/file_set_test.clj: -------------------------------------------------------------------------------- 1 | (ns bencode.java.file-set-test 2 | (:use clojure.test) 3 | (:require [clojure.java.io :as io]) 4 | (:import [bencode FileSet])) 5 | 6 | (def files (filter #(.isFile %) (file-seq (io/file "resources/fixtures/pictures")))) 7 | 8 | (deftest file-set-class 9 | (let [file-set (FileSet. files)] 10 | (testing "calculating total number of pieces" 11 | (is (= 13 (.totalPieces file-set (* 256 1024))))))) 12 | -------------------------------------------------------------------------------- /src/test/clojure/bencode/java/parallel_piece_digest_test.clj: -------------------------------------------------------------------------------- 1 | (ns bencode.java.parallel-piece-digest-test 2 | (:use clojure.test) 3 | (:require [clojure.java.io :as io]) 4 | (:import [bencode ParallelPieceDigest] 5 | [java.io File])) 6 | 7 | (def piece-length (* 256 1024)) 8 | 9 | (deftest parallel-piece-digest 10 | (testing "digesting file that fit in a piece" 11 | (let [files [(io/file "resources/fixtures/pictures/arches-9.jpg")] 12 | digest (ParallelPieceDigest. files piece-length) 13 | hash (.computeHash digest 2)] 14 | 15 | (testing "checking the number of pieces" 16 | (is (= 1 (/ (count hash) 20)))) 17 | 18 | (testing "checking the generated hash" 19 | (is (= '(-90 -89 102 2 93 54 -53 -92 -26 -103 -95 -126 -39 -103 -75 -78 -80 79 19 -3) 20 | (seq hash)))))) 21 | 22 | (testing "digesting two files that fit in a piece" 23 | (let [files [(io/file "resources/fixtures/pictures/arches-9.jpg") 24 | (io/file "resources/fixtures/pictures/beach.jpg")] 25 | digest (ParallelPieceDigest. files piece-length) 26 | hash (.computeHash digest 2)] 27 | 28 | (testing "checking the number of pieces" 29 | (is (= 1 (/ (count hash) 20)))) 30 | 31 | (testing "checking the generated hash" 32 | (is (= '(-30 -39 -91 -60 85 95 5 -89 -67 87 -23 45 41 -72 -118 120 74 84 124 -21) 33 | (seq hash)))))) 34 | 35 | (testing "digesting a file that don't fit in a piece" 36 | (let [files [(io/file "resources/fixtures/pictures/architecture.jpg")] 37 | digest (ParallelPieceDigest. files piece-length) 38 | hash (.computeHash digest 2)] 39 | 40 | (testing "checking the number of pieces" 41 | (is (= 12 (/ (count hash) 20)))) 42 | 43 | (testing "checking the generated hash" 44 | (is (= '(39 -75 90 106 -7 4 -113 -116 -51 94 75 4 -18 49 -120 -13 56 99 -17 35 45 | 50 -73 72 -100 85 -16 105 -28 -30 26 52 -71 76 -3 -76 105 69 -28 36 64 46 | 94 -89 26 4 -15 -96 71 106 121 79 -83 119 49 94 123 -65 93 42 46 85 47 | -104 -59 88 -47 -31 68 9 77 -101 -94 75 -93 -21 105 78 72 -18 -93 -5 34 48 | -100 -16 49 87 47 -78 -55 117 35 -42 -100 42 93 115 56 83 -126 37 103 105 49 | -63 -37 -96 -117 -76 -96 34 -33 -19 111 -83 55 -48 -20 69 -27 -43 -59 60 118 50 | 41 -66 -90 8 83 4 -23 27 -89 127 36 -65 93 17 -69 105 19 56 -53 93 51 | -42 9 115 118 0 10 -61 106 -37 -13 -59 77 -1 89 2 86 -97 121 22 19 52 | -126 -83 -72 -2 -7 -3 -17 11 22 13 124 2 -95 -107 75 92 -25 48 -112 67 53 | 11 63 -3 -78 76 -101 -26 102 6 23 38 -13 67 102 112 -70 -78 -69 91 13 54 | -120 -61 -52 -110 -16 96 84 57 19 -76 44 -60 -89 41 57 -71 45 6 82 64 55 | -112 54 -24 -86 -15 117 -102 -100 38 -25 -126 12 31 -93 -89 21 -25 -26 114 -108) 56 | (seq hash))))))) 57 | -------------------------------------------------------------------------------- /src/test/clojure/bencode/java/piece_digest_job_test.clj: -------------------------------------------------------------------------------- 1 | (ns bencode.java.piece-digest-job-test 2 | (:use clojure.test) 3 | (:require [clojure.java.io :as io]) 4 | (:import [bencode PieceDigestJob] 5 | [java.util.concurrent.atomic AtomicInteger])) 6 | 7 | (def data (byte-array (map byte (range 10)))) 8 | 9 | (deftest piece-hashing 10 | (testing "hashing first piece" 11 | (let [global-hash (byte-array 40) 12 | hashed-pieces (AtomicInteger.) 13 | job (PieceDigestJob. global-hash data (count data) 0 hashed-pieces) 14 | _ (.run job)] 15 | (testing "increments the number of hashed pieces" 16 | (is (= 1 (.get hashed-pieces)))) 17 | 18 | (testing "writes the digest result to the correct area of the global hash" 19 | (is (= '(73 65 121 113 74 108 -42 39 35 -99 -2 -34 -33 45 -23 -17 -103 76 -81 3 20 | 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0) 21 | (seq global-hash)))))) 22 | 23 | (testing "hashing incomplete last piece" 24 | (let [global-hash (byte-array 40) 25 | hashed-pieces (AtomicInteger. 1) 26 | job (PieceDigestJob. global-hash data (/ (count data) 2) 1 hashed-pieces) 27 | _ (.run job)] 28 | (testing "increments the number of hashed pieces" 29 | (is (= 2 (.get hashed-pieces)))) 30 | 31 | (testing "writes the digest result to the correct area of the global hash" 32 | (is (= '(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 33 | 28 -14 81 71 45 89 -8 -6 -34 -77 -85 37 -114 -112 -103 -99 -124 -111 -66 25) 34 | (seq global-hash))))))) 35 | -------------------------------------------------------------------------------- /src/test/clojure/bencode/metainfo/reader_test.clj: -------------------------------------------------------------------------------- 1 | (ns bencode.metainfo.reader-test 2 | (:require [clojure.java [io :as io]]) 3 | (:use [clojure.test] 4 | [bencode.core] 5 | [bencode.metainfo.reader]) 6 | (:import [java.util Date])) 7 | 8 | (defonce ^:dynamic *meta-info* nil) 9 | 10 | (defmacro with-torrent [file-name & body] 11 | `(binding [*meta-info* (parse-metainfo-file ~file-name)] 12 | ~@body)) 13 | 14 | (deftest meta-info-type 15 | (testing "Checking if a torrent only contains a single file" 16 | (with-torrent "resources/fixtures/single-file.torrent" 17 | (is (single-file-torrent? *meta-info*)) 18 | (is (not (multi-file-torrent? *meta-info*))))) 19 | 20 | (testing "Checking if a torrent contains multiple files" 21 | (with-torrent "resources/fixtures/multi-file.torrent" 22 | (is (multi-file-torrent? *meta-info*)) 23 | (is (not (single-file-torrent? *meta-info*)))))) 24 | 25 | (deftest torrent-name-utf-8 26 | (with-torrent "resources/fixtures/eceae16f50b11d3a6542b86aefdc1c6cb28ad708.torrent" 27 | (testing "Torrent name encoding by utf-8" 28 | (is (= "星球大战前传1-3" (get-in *meta-info* ["info" "name.utf-8"]))) 29 | (is (= "星球大战前传1-3" (torrent-name *meta-info*)))))) 30 | 31 | (deftest basic-info 32 | (with-torrent "resources/fixtures/single-file.torrent" 33 | (testing "Tracker URL" 34 | (is (= "udp://tracker.1337x.org:80/announce" 35 | (torrent-announce *meta-info*)))) 36 | 37 | (testing "Tracker URL list" 38 | (is (= [["udp://tracker.1337x.org:80/announce"] 39 | ["udp://tracker.publicbt.com:80/announce"] 40 | ["udp://tracker.openbittorrent.com:80/announce"] 41 | ["udp://fr33domtracker.h33t.com:3310/announce"] 42 | ["udp://tracker.istole.it:80/announce"] 43 | ["http://exodus.desync.com:6969/announce"] 44 | ["udp://tracker.ccc.de:80"] 45 | ["udp://tracker.istole.it:6969"] 46 | ["udp://tracker.openbittorrent.com:80"] 47 | ["udp://tracker.publicbt.com:80"]] 48 | (torrent-announce-list *meta-info*)))) 49 | 50 | (testing "Torrent name" 51 | (is (= "ubuntu-12.10-desktop-i386.iso" (torrent-name *meta-info*)))) 52 | 53 | (testing "Piece length, in bytes" 54 | (is (= 524288 (torrent-piece-length *meta-info*)))) 55 | 56 | (testing "Number of pieces" 57 | (is (= 1507 (torrent-pieces-count *meta-info*)))) 58 | 59 | (testing "Pieces hash" 60 | (testing "Hash for the first piece" 61 | (is (= "0fb30249ec80648eebe61636396d9a247068d5a2" 62 | (first (torrent-pieces-hash-list *meta-info*))))) 63 | 64 | (testing "Hash for the last piece" 65 | (is (= "ad649e2955576e0af98200564cd4c5e2da081453" 66 | (last (torrent-pieces-hash-list *meta-info*)))))) 67 | 68 | (testing "Creation date" 69 | (is (= (Date. (* 1350570562 1000)) 70 | (torrent-creation-date *meta-info*)))) 71 | 72 | (testing "Comment" 73 | (is (= "http://www.monova.org" (torrent-comment *meta-info*)))) 74 | 75 | (testing "Private" 76 | (is (public-torrent? *meta-info*)) 77 | (is (not (private-torrent? *meta-info*)))))) 78 | 79 | (deftest private-torrent-info 80 | (with-torrent "resources/fixtures/private.torrent" 81 | (testing "Private" 82 | (is (private-torrent? *meta-info*)) 83 | (is (not (public-torrent? *meta-info*)))) 84 | 85 | (testing "Created by" 86 | (is (= "mktorrent 1.0" (torrent-created-by *meta-info*)))))) 87 | 88 | (deftest single-file-torrent-info 89 | (with-torrent "resources/fixtures/single-file.torrent" 90 | (testing "Torrent size" 91 | (is (= 789884928 (torrent-size *meta-info*)))) 92 | 93 | (testing "Torrent info hash as a byte array" 94 | (let [hash (torrent-info-hash *meta-info*)] 95 | (is (instance? (Class/forName "[B") hash)) 96 | (is (= '(51 89 -112 -42 21 89 75 -101 -28 9 -52 -2 -71 88 100 -30 78 -57 2 -57) 97 | (seq hash))))) 98 | 99 | (testing "Torrent info hash as string" 100 | (is (= "335990d615594b9be409ccfeb95864e24ec702c7" 101 | (torrent-info-hash-str *meta-info*)))))) 102 | 103 | (deftest multi-file-torrent-info 104 | (with-torrent "resources/fixtures/multi-file.torrent" 105 | (testing "Torrent size is the sum of the sizes of all files" 106 | (is (= 5805771164 (torrent-size *meta-info*)))) 107 | 108 | (testing "Torrent info hash as a byte array" 109 | (let [hash (torrent-info-hash *meta-info*)] 110 | (is (instance? (Class/forName "[B") hash)) 111 | (is (= '(-124 -118 106 14 -58 -56 85 7 -72 55 14 -105 -101 19 50 20 -27 -75 -90 -44) 112 | (seq hash))))) 113 | 114 | (testing "Torrent info hash as string" 115 | (is (= "848a6a0ec6c85507b8370e979b133214e5b5a6d4" 116 | (torrent-info-hash-str *meta-info*)))) 117 | 118 | (testing "Torrent files" 119 | (is (= [{"length" 4353378304, "path" ["CentOS-6.4-x86_64-bin-DVD1.iso"]} 120 | {"length" 1452388352, "path" ["CentOS-6.4-x86_64-bin-DVD2.iso"]} 121 | {"length" 261, "path" ["md5sum.txt"]} 122 | {"length" 1135, "path" ["md5sum.txt.asc"]} 123 | {"length" 293, "path" ["sha1sum.txt"]} 124 | {"length" 1167, "path" ["sha1sum.txt.asc"]} 125 | {"length" 389, "path" ["sha256sum.txt"]} 126 | {"length" 1263, "path" ["sha256sum.txt.asc"]}] 127 | (torrent-files *meta-info*)))))) 128 | 129 | (deftest magnet-link 130 | (with-torrent "resources/fixtures/multi-file.torrent" 131 | (testing "Magnet link generation" 132 | (is (= "magnet:?xt=urn:btih:848a6a0ec6c85507b8370e979b133214e5b5a6d4&dn=CentOS-6.4-x86_64-bin-DVD1to2&tr=udp%3A%2F%2Ftracker.publicbt.com%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80%2Fannounce&tr=udp%3A%2F%2Ffr33domtracker.h33t.com%3A3310%2Fannounce&tr=udp%3A%2F%2Ftracker.istole.it%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.feednet.ro%3A80%2Fannounce&tr=http%3A%2F%2Fipv6.torrent.centos.org%3A6969%2Fannounce&tr=http%3A%2F%2Ftorrent.centos.org%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.ccc.de%3A80&tr=udp%3A%2F%2Ftracker.istole.it%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=udp%3A%2F%2Ftracker.publicbt.com%3A80" 133 | (torrent-magnet-link *meta-info*)))))) 134 | -------------------------------------------------------------------------------- /src/test/clojure/bencode/metainfo/writer_test.clj: -------------------------------------------------------------------------------- 1 | (ns bencode.metainfo.writer-test 2 | (:require [clojure.java.io :as io]) 3 | (:use [clojure.test] 4 | [bencode.metainfo.writer])) 5 | 6 | (deftest torrent-file-path 7 | (testing "array of file path relative to a given parent directory" 8 | (let [dir (io/file "resources/fixtures") 9 | file (io/file "resources/fixtures/pictures/beach.jpg")] 10 | (is (= ["pictures" "beach.jpg"] 11 | (file-path dir file)))))) 12 | 13 | (deftest torrent-file-entry 14 | (testing "dictionary denoting a file in multi-file torrent" 15 | (let [dir (io/file "resources/fixtures") 16 | file (io/file "resources/fixtures/pictures/beach.jpg")] 17 | (is (= {"length" (.length file) "path" (file-path dir file)} 18 | (file-entry dir file)))))) 19 | 20 | (deftest torrent-scan-files 21 | (testing "filtering files from a seq of File" 22 | (let [all (io/file "resources/fixtures/pictures")] 23 | (is (every? #(.isFile %) (scan-files all)))))) 24 | 25 | (def torrent-file-entries 26 | (testing "list of dictionaries for each file in a multi-file torrent" 27 | (let [dir (io/file "resources/fixtures/pictures") 28 | files (scan-files dir) 29 | entries (torrent-files dir files)] 30 | (doseq [entry entries] 31 | (is (> (entry "length") 0)) 32 | (is (not (empty? (entry "path")))))))) 33 | 34 | (deftest piece-length 35 | (testing "default piece length should be 256 KiB" 36 | (is (= (* 1024 256) (torrent-piece-length nil)))) 37 | 38 | (testing "piece length should be 2^n KiB" 39 | (is (= (* 1024 512) (torrent-piece-length 9))))) 40 | 41 | (deftest main-announce-url 42 | (testing "main announce URL should be the first one" 43 | (let [urls [["http://group-1.com/1" "http://group-1.com/2"] 44 | ["http://group-2.com/1"]]] 45 | (is (= "http://group-1.com/1" 46 | (torrent-main-announce urls)))))) 47 | -------------------------------------------------------------------------------- /src/test/clojure/bencode/type/dict_test.clj: -------------------------------------------------------------------------------- 1 | (ns bencode.type.dict-test 2 | (:use [clojure.test] 3 | [bencode.core])) 4 | 5 | (deftest dict-encoding 6 | (testing "Encoding an empty dictionary" 7 | (is (= "de" (bencode {})))) 8 | 9 | (testing "Encoding a simple non-empty dictionary" 10 | (is (= "d3:cow3:moo3:onei2e4:spami64ee" 11 | (bencode {"spam" 64, "cow" "moo", :one 2})))) 12 | 13 | (testing "Encoding a dictionary containing a sequence" 14 | (is (= "d4:spaml4:eggsi64eee" 15 | (bencode {:spam ["eggs" 64]})))) 16 | 17 | (testing "Encoding nested dictionaries" 18 | (is (= "d4:spamd3:cow3:mooee" 19 | (bencode {:spam {:cow "moo"}})))) 20 | 21 | (testing "Encoding a dictionary with invalid keys" 22 | (are [key] (thrown? IllegalArgumentException (bencode {key "val"})) 23 | [] {} 64))) 24 | 25 | (deftest dict-decoding 26 | (testing "Use a sorted map" 27 | (is (sorted? (bdecode "de")))) 28 | 29 | (testing "Decoding an empty dictionary" 30 | (is (= {} 31 | (bdecode "de")))) 32 | 33 | (testing "Decoding a simple non-empty dictionary" 34 | (is (= {:spam 64 :cow "moo"} 35 | (bdecode "d3:cow3:moo4:spami64ee")))) 36 | 37 | (testing "Decoding a dictionary containing a sequence" 38 | (is (= {:spam ["eggs" 64]} 39 | (bdecode "d4:spaml4:eggsi64eee")))) 40 | 41 | (testing "Decoding nested dictionaries" 42 | (is (= {:spam {:cow "moo"}} 43 | (bdecode "d4:spamd3:cow3:mooee")))) 44 | 45 | (testing "Decoding a dictionary with invalid keys" 46 | (is (thrown? IllegalArgumentException 47 | (bdecode "di64e3:cowe"))))) 48 | 49 | (deftest decoding-options 50 | (testing "Decoding keys as strings instead of keywords" 51 | (is (= {"spam" {"cow" "moo"}}) 52 | (bdecode "d4:spamd3:cow3:mooee" {:str-keys? true}))) 53 | 54 | (testing "Decoding dict value as byte array" 55 | (let [result (bdecode "d3:cow3:moo3:onei2e4:spami64ee" {:raw-keys ["cow"]})] 56 | (is (= 2 (:one result))) 57 | (is (= 64 (:spam result))) 58 | (is (= (vec (byte-array (map byte "moo"))) 59 | (vec (:cow result))))))) 60 | -------------------------------------------------------------------------------- /src/test/clojure/bencode/type/list_test.clj: -------------------------------------------------------------------------------- 1 | (ns bencode.type.list-test 2 | (:use [clojure.test] 3 | [bencode.core])) 4 | 5 | (deftest sequence-decoding 6 | (testing "Decoding an empty sequence" 7 | (is (= [] 8 | (bdecode "le")))) 9 | 10 | (testing "Decoding a simple non-empty sequence" 11 | (is (= ["spam" "eggs" 64] 12 | (bdecode "l4:spam4:eggsi64ee")))) 13 | 14 | (testing "Decoding nested sequences" 15 | (is (= ["spam" ["eggs" 64]] 16 | (bdecode "l4:spaml4:eggsi64eee"))))) 17 | 18 | (deftest sequence-encoding 19 | (testing "Encoding an empty sequence" 20 | (is (= "le" 21 | (bencode [])))) 22 | 23 | (testing "Encoding a simple non-empty sequence" 24 | (is (= "l4:spam4:eggsi64ee" 25 | (bencode (seq ["spam" "eggs" 64]))))) 26 | 27 | (testing "Encoding nested sequences" 28 | (is (= "l4:spaml4:eggsi64eee" 29 | (bencode #{"spam" ["eggs" 64]}))))) 30 | 31 | -------------------------------------------------------------------------------- /src/test/clojure/bencode/type/number_test.clj: -------------------------------------------------------------------------------- 1 | (ns bencode.type.number-test 2 | (:use [clojure.test] 3 | [bencode.core])) 4 | 5 | (deftest encoding-unsigned-numbers 6 | (testing "Encoding zero" 7 | (is (= "i0e" 8 | (bencode 0)))) 9 | 10 | (testing "Encoding a positive number" 11 | (is (= "i64e" 12 | (bencode 64)))) 13 | 14 | (testing "Encoding a very large number" 15 | (are [n] (= "i238273467862384962834523482364273525365425364825376547257e" 16 | (bencode n)) 17 | (BigInteger. "238273467862384962834523482364273525365425364825376547257") 18 | 238273467862384962834523482364273525365425364825376547257N))) 19 | 20 | (deftest encoding-signed-numbers 21 | (testing "Encoding a negative number" 22 | (is (= "i-32e" 23 | (bencode -32))))) 24 | 25 | (deftest decoding-unsigned-numbers 26 | (testing "Decoding zero" 27 | (is (zero? 28 | (bdecode "i0e")))) 29 | 30 | (testing "Decoding a positive number" 31 | (is (= 64 32 | (bdecode "i64e")))) 33 | 34 | (testing "Decoding a very large number" 35 | (is (= 238273467862384962834523482364273525365425364825376547257N 36 | (bdecode "i238273467862384962834523482364273525365425364825376547257e"))))) 37 | 38 | (deftest decoding-signed-numbers 39 | (testing "Decoding a positive number with sign" 40 | (is (= 64 41 | (bdecode "i+64e")))) 42 | 43 | (testing "Decoding a negative number" 44 | (is (= -32 45 | (bdecode "i-32e"))))) 46 | 47 | (deftest decoding-invalid-numbers 48 | (testing "Decoding octal number" 49 | (is (thrown? IllegalArgumentException 50 | (bdecode "i010e")))) 51 | 52 | (testing "Decoding empty number" 53 | (is (thrown? IllegalArgumentException 54 | (bdecode "ie")))) 55 | 56 | (testing "Decoding empty number with sign" 57 | (are [s] (thrown? IllegalArgumentException 58 | (bdecode s)) 59 | "i-e" "i+e")) 60 | 61 | (testing "Decoding a number with more than one sign" 62 | (is (thrown? IllegalArgumentException 63 | (bdecode "i+-10e")))) 64 | 65 | (testing "Decoding invalid 'negative zero'" 66 | (is (thrown? IllegalArgumentException 67 | (bdecode "i-0e")))) 68 | 69 | (testing "Decoding multiple numbers inside the same unit" 70 | (is (thrown? IllegalArgumentException 71 | (bdecode "i30-20e")))) 72 | 73 | (testing "Decoding number with invalid terminator" 74 | (is (thrown? IllegalArgumentException 75 | (bdecode "i10x"))))) 76 | -------------------------------------------------------------------------------- /src/test/clojure/bencode/type/string_test.clj: -------------------------------------------------------------------------------- 1 | (ns bencode.type.string-test 2 | (:use [clojure.test] 3 | [bencode.core])) 4 | 5 | (deftest string-encoding 6 | (testing "Encoding a non-empty string" 7 | (is (= "4:spam" 8 | (bencode "spam")))) 9 | 10 | (testing "Encoding an empty string" 11 | (is (= "0:" 12 | (bencode "")))) 13 | 14 | (testing "Encoding an string respecing the encoding" 15 | (is (= "6:2000\u0445" 16 | (bencode "2000\u0445"))))) 17 | 18 | (deftest bytes-encoding 19 | (testing "Encoding a byte-array" 20 | (is (= "4:spam" 21 | (bencode (.getBytes "spam")))))) 22 | 23 | (deftest keyword-encoding 24 | (testing "Encoding a keyword" 25 | (is (= "4:spam" 26 | (bencode :spam))))) 27 | 28 | (deftest string-decoding 29 | (testing "Decoding a non-empty string" 30 | (is (= "spam" 31 | (bdecode "4:spam")))) 32 | 33 | (testing "Decoding an empty string" 34 | (is (= "" 35 | (bdecode "0:")))) 36 | 37 | (testing "Decoding a UTF-8 string" 38 | (is (= "2000\u0445" 39 | (bdecode (.getBytes "6:2000\u0445" "UTF-8"))))) 40 | 41 | (testing "Decoding a string that is shorter than expected" 42 | (is (thrown? IllegalArgumentException 43 | (bdecode "4:spa")))) 44 | 45 | (testing "Decoding a non-empty string with garbage at the end" 46 | (is (thrown? IllegalArgumentException 47 | (bdecode "4:spam0:"))))) 48 | 49 | (deftest decoding-options 50 | (testing "Decoding strings as a byte array" 51 | (is (= (vec (.getBytes "spam")) 52 | (vec (bdecode "4:spam" {:raw-str? true})))))) 53 | -------------------------------------------------------------------------------- /src/test/clojure/bencode/type/unsupported_test.clj: -------------------------------------------------------------------------------- 1 | (ns bencode.type.unsupported-test 2 | (:use [clojure.test] 3 | [bencode.core])) 4 | 5 | (deftest unsupported-encoding 6 | (testing "Encoding unsupported data types" 7 | (are [type] (thrown? IllegalArgumentException (bencode type)) 8 | (float 10) (double 10) (bigdec 10)))) 9 | 10 | (deftest unsupported-decoding 11 | (testing "Decoding unknown type" 12 | (is (thrown? IllegalArgumentException 13 | (bdecode "x10"))))) 14 | -------------------------------------------------------------------------------- /src/test/clojure/bencode/utils_test.clj: -------------------------------------------------------------------------------- 1 | (ns bencode.utils-test 2 | (:use clojure.test 3 | bencode.utils)) 4 | 5 | (deftest int-flag-from-bool 6 | (testing "should return 1 if true" 7 | (is (= 1 (flag-from-bool true)))) 8 | 9 | (testing "should return 0 if false" 10 | (is (zero? (flag-from-bool false))))) 11 | 12 | (deftest is-digit 13 | (testing "Detecting valid digits" 14 | (doseq [ch (apply str (range 10))] 15 | (is (digit? (int ch))))) 16 | 17 | (testing "Detecting invalid digits" 18 | (doseq [ch "azAZ"] 19 | (is (not (digit? (int ch))))))) 20 | --------------------------------------------------------------------------------