├── .gitignore ├── ci-settings.xml ├── src ├── main │ └── java │ │ └── com │ │ └── dampcake │ │ └── bencode │ │ ├── Validator.java │ │ ├── StringValidator.java │ │ ├── BencodeException.java │ │ ├── TypeValidator.java │ │ ├── Type.java │ │ ├── BencodeOutputStream.java │ │ ├── Bencode.java │ │ └── BencodeInputStream.java └── test │ └── java │ └── com │ └── dampcake │ └── bencode │ ├── BencodeOutputStreamTest.java │ ├── BencodeInputStreamTest.java │ └── BencodeTest.java ├── .github └── workflows │ └── ci.yml ├── README.md ├── pom.xml └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | .DS_Store 3 | 4 | # Mobile Tools for Java (J2ME) 5 | .mtj.tmp/ 6 | 7 | # IntelliJ 8 | *.iml 9 | .idea/* 10 | 11 | # VSCode/Eclipse 12 | .settings 13 | .project 14 | .classpath 15 | .vscode 16 | 17 | # Maven 18 | target 19 | 20 | # Package Files # 21 | *.jar 22 | *.war 23 | *.ear 24 | 25 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 26 | hs_err_pid* 27 | -------------------------------------------------------------------------------- /ci-settings.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | central 9 | ${env.SONATYPE_USERNAME} 10 | ${env.SONATYPE_PASSWORD} 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/java/com/dampcake/bencode/Validator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Adam Peck. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.dampcake.bencode; 17 | 18 | /** 19 | * Validator interface. 20 | * 21 | * @author Adam Peck 22 | */ 23 | interface Validator { 24 | boolean validate(final int token); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/dampcake/bencode/StringValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Adam Peck. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.dampcake.bencode; 17 | 18 | /** 19 | * Validates String tokens. 20 | * 21 | * @author Adam Peck 22 | */ 23 | final class StringValidator implements Validator { 24 | 25 | public boolean validate(final int token) { 26 | return Character.isDigit(token); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/dampcake/bencode/BencodeException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Adam Peck. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.dampcake.bencode; 17 | 18 | /** 19 | * Wraps a {@link Throwable} that is thrown during decode/encode. 20 | * 21 | * @author Adam Peck 22 | * @since 1.1 23 | */ 24 | public class BencodeException extends RuntimeException { 25 | 26 | BencodeException(String message, Throwable cause) { 27 | super(message, cause); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/dampcake/bencode/TypeValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Adam Peck. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.dampcake.bencode; 17 | 18 | /** 19 | * Validates non-String types. 20 | * 21 | * @author Adam Peck 22 | */ 23 | final class TypeValidator implements Validator { 24 | 25 | private final char type; 26 | 27 | public TypeValidator(final char type) { 28 | this.type = type; 29 | } 30 | 31 | public boolean validate(final int token) { 32 | return token == type; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | java: [ 8, 11, 17, 21, 23, 24 ] 15 | env: 16 | JAVA: ${{ matrix.java }} 17 | name: Java ${{ matrix.java }} 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Setup Java 21 | uses: actions/setup-java@v4 22 | with: 23 | distribution: 'zulu' 24 | java-version: ${{ matrix.java }} 25 | - name: Test 26 | run: mvn -B test jacoco:report 27 | - uses: codecov/codecov-action@v5 28 | with: 29 | fail_ci_if_error: true 30 | token: ${{ secrets.CODECOV_TOKEN }} 31 | env_vars: JAVA 32 | deploy: 33 | runs-on: ubuntu-latest 34 | needs: test 35 | if: github.ref == 'refs/heads/main' 36 | name: Deploy 37 | steps: 38 | - uses: actions/checkout@v4 39 | - name: Setup Java 40 | uses: actions/setup-java@v4 41 | with: 42 | distribution: 'zulu' 43 | java-version: 8 44 | - name: Deploy 45 | env: 46 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 47 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 48 | run: mvn -B clean deploy --settings ci-settings.xml -DskipTests 49 | -------------------------------------------------------------------------------- /src/main/java/com/dampcake/bencode/Type.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Adam Peck. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.dampcake.bencode; 17 | 18 | import java.util.List; 19 | import java.util.Map; 20 | 21 | /** 22 | * Data Types in bencode. 23 | * 24 | * @author Adam Peck 25 | */ 26 | public class Type { 27 | 28 | public static final Type STRING = new Type(new StringValidator()); 29 | public static final Type NUMBER = new Type(new TypeValidator(Bencode.NUMBER)); 30 | public static final Type> LIST = new Type>(new TypeValidator(Bencode.LIST)); 31 | public static final Type> DICTIONARY = new Type>(new TypeValidator(Bencode.DICTIONARY)); 32 | public static final Type UNKNOWN = new Type(new Validator() { 33 | public boolean validate(int token) { 34 | return false; 35 | } 36 | }); 37 | 38 | private final Validator validator; 39 | 40 | private Type(final Validator validator) { 41 | this.validator = validator; 42 | } 43 | 44 | boolean validate(final int token) { 45 | return validator.validate(token); 46 | } 47 | 48 | public static Type[] values() { 49 | return new Type[] { STRING, NUMBER, LIST, DICTIONARY, UNKNOWN }; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bencode 2 | 3 | [![Build Status](https://github.com/dampcake/bencode/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/dampcake/bencode/actions?query=branch%3Amain) 4 | [![Coverage Status](https://codecov.io/gh/dampcake/bencode/branch/main/graph/badge.svg)](https://codecov.io/gh/dampcake/bencode) 5 | [![Maven](https://img.shields.io/maven-central/v/com.dampcake/bencode.svg)](http://search.maven.org/#search%7Cga%7C1%7Ccom.dampcake.bencode) 6 | [![GitHub license](https://img.shields.io/github/license/dampcake/bencode.svg)](https://github.com/dampcake/bencode/blob/main/LICENSE) 7 | 8 | Bencode Input/Output Streams for Java 9 | 10 | Requires JDK 1.8 or higher 11 | 12 | [Bencode Spec](https://wiki.theory.org/BitTorrentSpecification#Bencoding) 13 | 14 | [Bencode Wikipedia](https://en.wikipedia.org/wiki/Bencode) 15 | 16 | ## Javadoc 17 | http://dampcake.github.io/bencode 18 | 19 | ## Usage 20 | 21 | ### Maven 22 | ```xml 23 | 24 | com.dampcake 25 | bencode 26 | 1.4.2 27 | 28 | ``` 29 | 30 | ### Gradle 31 | ```groovy 32 | compile 'com.dampcake:bencode:1.4.2' 33 | ``` 34 | 35 | ### Examples 36 | 37 | #### Bencode Data 38 | ```java 39 | Bencode bencode = new Bencode(); 40 | byte[] encoded = bencode.encode(new HashMap() {{ 41 | put("string", "value"); 42 | put("number", 123456); 43 | put("list", new ArrayList() {{ 44 | add("list-item-1"); 45 | add("list-item-2"); 46 | }}); 47 | put("dict", new ConcurrentSkipListMap() {{ 48 | put(123, "test"); 49 | put(456, "thing"); 50 | }}); 51 | }}); 52 | 53 | System.out.println(new String(encoded, bencode.getCharset())); 54 | ``` 55 | 56 | Outputs: ```d4:dictd3:1234:test3:4565:thinge4:listl11:list-item-111:list-item-2e6:numberi123456e6:string5:valuee``` 57 | 58 | #### Decode Bencoded Data: 59 | ```java 60 | Bencode bencode = new Bencode(); 61 | Map dict = bencode.decode("d4:dictd3:1234:test3:4565:thinge4:listl11:list-item-111:list-item-2e6:numberi123456e6:string5:valuee".getBytes(), Type.DICTIONARY); 62 | 63 | System.out.println(dict); 64 | ``` 65 | 66 | Outputs: ```{dict={123=test, 456=thing}, list=[list-item-1, list-item-2], number=123456, string=value}``` 67 | 68 | #### Write bencoded data to a Stream: 69 | ```java 70 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 71 | BencodeOutputStream bencoder = new BencodeOutputStream(out); 72 | 73 | bencoder.writeDictionary(new HashMap() {{ 74 | put("string", "value"); 75 | put("number", 123456); 76 | put("list", new ArrayList() {{ 77 | add("list-item-1"); 78 | add("list-item-2"); 79 | }}); 80 | put("dict", new ConcurrentSkipListMap() {{ 81 | put("dict-item-1", "test"); 82 | put("dict-item-2", "thing"); 83 | }}); 84 | }}); 85 | 86 | System.out.println(new String(out.toByteArray())); 87 | ``` 88 | 89 | Outputs: ```d4:dictd11:dict-item-14:test11:dict-item-25:thinge4:listl11:list-item-111:list-item-2e6:numberi123456e6:string5:valuee``` 90 | 91 | #### Read bencoded data to a Stream: 92 | ```java 93 | String input = "d4:dictd11:dict-item-14:test11:dict-item-25:thinge4:listl11:list-item-111:list-item-2e6:numberi123456e6:string5:valuee"; 94 | ByteArrayInputStream in = new ByteArrayInputStream(input.getBytes()); 95 | BencodeInputStream bencode = new BencodeInputStream(in); 96 | 97 | Type type = bencode.nextType(); // Returns Type.DICTIONARY 98 | Map dict = bencode.readDictionary(); 99 | 100 | System.out.println(dict); 101 | ``` 102 | 103 | Outputs: ```{dict={dict-item-1=test, dict-item-2=thing}, list=[list-item-1, list-item-2], number=123456, string=value}``` 104 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | com.dampcake 5 | bencode 6 | 1.4.3-SNAPSHOT 7 | jar 8 | 9 | bencode 10 | Input/Output Streams for working with bencoded data. 11 | http://github.com/dampcake/bencode 12 | 2015 13 | 14 | 15 | 16 | The Apache Software License, Version 2.0 17 | http://www.apache.org/licenses/LICENSE-2.0.txt 18 | repo 19 | 20 | 21 | 22 | 23 | scm:git:https://github.com/dampcake/bencode.git 24 | scm:git:https://github.com/dampcake/bencode.git 25 | https://github.com/dampcake/bencode 26 | HEAD 27 | 28 | 29 | 30 | GitHub Issues 31 | https://github.com/dampcake/bencode/issues 32 | 33 | 34 | 35 | 36 | dampcake 37 | Adam Peck 38 | adam.peck@live.ca 39 | 40 | owner 41 | developer 42 | 43 | -7 44 | 45 | 46 | 47 | 48 | 49 | junit 50 | junit 51 | 4.13.2 52 | test 53 | 54 | 55 | 56 | 57 | UTF-8 58 | 59 | 60 | 61 | 62 | 63 | release-sign-artifacts 64 | 65 | 66 | performRelease 67 | true 68 | 69 | 70 | 71 | 72 | 73 | org.apache.maven.plugins 74 | maven-gpg-plugin 75 | 3.2.7 76 | 77 | 78 | sign-artifacts 79 | verify 80 | 81 | sign 82 | 83 | 84 | 85 | --pinentry-mode 86 | loopback 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | package 99 | 100 | 101 | org.apache.maven.plugins 102 | maven-compiler-plugin 103 | 3.14.0 104 | 105 | 1.8 106 | 1.8 107 | -Xlint:unchecked 108 | 109 | 110 | 111 | maven-source-plugin 112 | 3.3.1 113 | 114 | 115 | attach-sources 116 | 117 | jar 118 | 119 | 120 | 121 | 122 | 123 | org.apache.maven.plugins 124 | maven-javadoc-plugin 125 | 3.11.2 126 | 127 | public 128 | true 129 |
bencode, ${project.version}
130 |
bencode, ${project.version}
131 | bencode, ${project.version} 132 |
133 | 134 | 135 | attach-javadocs 136 | 137 | jar 138 | 139 | 140 | 141 |
142 | 143 | 144 | org.jacoco 145 | jacoco-maven-plugin 146 | 0.8.12 147 | 148 | 149 | 150 | prepare-agent 151 | 152 | 153 | 154 | report 155 | test 156 | 157 | report 158 | 159 | 160 | 161 | 162 | 163 | 164 | org.apache.maven.plugins 165 | maven-release-plugin 166 | 3.1.0 167 | 168 | release-sign-artifacts 169 | 170 | 171 | 172 | 173 | 174 | org.apache.maven.plugins 175 | maven-scm-publish-plugin 176 | 3.3.0 177 | 178 | ${project.build.directory}/scmpublish 179 | Publishing javadoc for ${project.artifactId}:${project.version} 180 | ${project.build.directory}/reports/apidocs 181 | true 182 | scm:git:https://github.com/dampcake/bencode.git 183 | 184 | gh-pages 185 | 186 | 187 | 188 | 189 | org.sonatype.central 190 | central-publishing-maven-plugin 191 | 0.7.0 192 | true 193 | 194 | central 195 | 196 | 197 |
198 |
199 |
200 | -------------------------------------------------------------------------------- /src/test/java/com/dampcake/bencode/BencodeOutputStreamTest.java: -------------------------------------------------------------------------------- 1 | package com.dampcake.bencode; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.junit.function.ThrowingRunnable; 6 | 7 | import java.io.ByteArrayOutputStream; 8 | import java.nio.ByteBuffer; 9 | import java.util.ArrayList; 10 | import java.util.HashMap; 11 | import java.util.LinkedHashMap; 12 | import java.util.TreeMap; 13 | import java.util.concurrent.ConcurrentSkipListMap; 14 | 15 | import static org.junit.Assert.assertEquals; 16 | import static org.junit.Assert.assertThrows; 17 | 18 | /** 19 | * Unit tests for BencodeOutputStream. 20 | * 21 | * @author Adam Peck 22 | */ 23 | public class BencodeOutputStreamTest { 24 | 25 | private ByteArrayOutputStream out; 26 | private BencodeOutputStream instance; 27 | 28 | @Before 29 | public void setUp() { 30 | out = new ByteArrayOutputStream(); 31 | instance = new BencodeOutputStream(out); 32 | } 33 | 34 | @Test 35 | @SuppressWarnings("resource") 36 | public void testConstructorNullStream() throws Exception { 37 | new BencodeOutputStream(null); 38 | } 39 | 40 | @Test(expected = NullPointerException.class) 41 | @SuppressWarnings("resource") 42 | public void testConstructorNullCharset() throws Exception { 43 | new BencodeOutputStream(out, null); 44 | } 45 | 46 | @Test 47 | public void testWriteString() throws Exception { 48 | instance.writeString("Hello World!"); 49 | 50 | assertEquals("12:Hello World!", new String(out.toByteArray(), instance.getCharset())); 51 | } 52 | 53 | @Test 54 | public void testWriteStringEmpty() throws Exception { 55 | instance.writeString(""); 56 | 57 | assertEquals("0:", new String(out.toByteArray(), instance.getCharset())); 58 | } 59 | 60 | @Test 61 | public void testWriteStringNull() throws Exception { 62 | assertThrows(NullPointerException.class, () -> instance.writeString((String) null)); 63 | assertEquals(0, out.toByteArray().length); 64 | } 65 | 66 | @Test 67 | public void testWriteStringByteBuffer() throws Exception { 68 | instance.writeString(ByteBuffer.wrap("Hello World!".getBytes())); 69 | 70 | assertEquals("12:Hello World!", new String(out.toByteArray(), instance.getCharset())); 71 | } 72 | 73 | @Test 74 | public void testWriteStringEmptyByteBuffer() throws Exception { 75 | instance.writeString(ByteBuffer.wrap(new byte[0])); 76 | 77 | assertEquals("0:", new String(out.toByteArray(), instance.getCharset())); 78 | } 79 | 80 | @Test 81 | public void testWriteStringByteArray() throws Exception { 82 | instance.writeString("Hello World!".getBytes()); 83 | 84 | assertEquals("12:Hello World!", new String(out.toByteArray(), instance.getCharset())); 85 | } 86 | 87 | @Test 88 | public void testWriteStringEmptyByteArray() throws Exception { 89 | instance.writeString(new byte[0]); 90 | 91 | assertEquals("0:", new String(out.toByteArray(), instance.getCharset())); 92 | } 93 | 94 | @Test 95 | public void testWriteStringNullByteBuffer() throws Exception { 96 | assertThrows(NullPointerException.class, () -> instance.writeString((ByteBuffer) null)); 97 | assertEquals(0, out.toByteArray().length); 98 | } 99 | 100 | @Test 101 | public void testWriteStringNullByteArray() throws Exception { 102 | assertThrows(NullPointerException.class, () -> instance.writeString((byte[]) null)); 103 | assertEquals(0, out.toByteArray().length); 104 | } 105 | 106 | @Test 107 | public void testWriteNumber() throws Exception { 108 | instance.writeNumber(123456); 109 | 110 | assertEquals("i123456e", new String(out.toByteArray(), instance.getCharset())); 111 | } 112 | 113 | @Test 114 | public void testWriteNumberDecimal() throws Exception { 115 | instance.writeNumber(123.456); 116 | 117 | assertEquals("i123e", new String(out.toByteArray(), instance.getCharset())); 118 | } 119 | 120 | @Test 121 | public void testWriteNumberNull() throws Exception { 122 | assertThrows(NullPointerException.class, () -> instance.writeNumber(null)); 123 | assertEquals(0, out.toByteArray().length); 124 | } 125 | 126 | @Test 127 | public void testWriteList() throws Exception { 128 | instance.writeList(new ArrayList() {{ 129 | add("Hello"); 130 | add(ByteBuffer.wrap("World!".getBytes())); 131 | add(new ArrayList() {{ 132 | add(123); 133 | add(456); 134 | }}); 135 | add("Foo".getBytes()); 136 | }}); 137 | 138 | assertEquals("l5:Hello6:World!li123ei456ee3:Fooe", new String(out.toByteArray(), instance.getCharset())); 139 | } 140 | 141 | @Test 142 | public void testWriteListEmpty() throws Exception { 143 | instance.writeList(new ArrayList()); 144 | 145 | assertEquals("le", new String(out.toByteArray(), instance.getCharset())); 146 | } 147 | 148 | @Test 149 | public void testWriteListNullItem() throws Exception { 150 | ThrowingRunnable runnable = () -> instance.writeList(new ArrayList() {{ 151 | add("Hello"); 152 | add(ByteBuffer.wrap("World!".getBytes())); 153 | add(new ArrayList() {{ 154 | add(null); 155 | add(456); 156 | }}); 157 | }}); 158 | assertThrows(NullPointerException.class, runnable); 159 | assertEquals(0, out.toByteArray().length); 160 | } 161 | 162 | @Test 163 | public void testWriteListNull() throws Exception { 164 | assertThrows(NullPointerException.class, () -> instance.writeList(null)); 165 | assertEquals(0, out.toByteArray().length); 166 | } 167 | 168 | @Test 169 | public void testWriteDictionary() throws Exception { 170 | instance.writeDictionary(new LinkedHashMap() {{ 171 | put("string", "value"); 172 | put("number", 123456); 173 | put("list", new ArrayList() {{ 174 | add("list-item-1"); 175 | add("list-item-2"); 176 | }}); 177 | put("dict", new ConcurrentSkipListMap() {{ 178 | put(123, ByteBuffer.wrap("test".getBytes())); 179 | put(456, "thing"); 180 | }}); 181 | }}); 182 | 183 | assertEquals("d4:dictd3:1234:test3:4565:thinge4:listl11:list-item-111:list-item-2e6:numberi123456e6:string5:valuee", 184 | new String(out.toByteArray(), instance.getCharset())); 185 | } 186 | 187 | @Test 188 | public void testWriteDictionaryEmpty() throws Exception { 189 | instance.writeDictionary(new HashMap()); 190 | 191 | assertEquals("de", new String(out.toByteArray(), instance.getCharset())); 192 | } 193 | 194 | @Test 195 | public void testWriteDictionaryKeyCastException() throws Exception { 196 | ThrowingRunnable runnable = () -> instance.writeDictionary(new TreeMap() {{ 197 | put("string", "value"); 198 | put(123, "number-key"); 199 | }}); 200 | assertThrows(ClassCastException.class, runnable); 201 | assertEquals(0, out.toByteArray().length); 202 | } 203 | 204 | @Test 205 | public void testWriteDictionaryNull() throws Exception { 206 | assertThrows(NullPointerException.class, () -> instance.writeDictionary(null)); 207 | assertEquals(0, out.toByteArray().length); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/main/java/com/dampcake/bencode/BencodeOutputStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Adam Peck. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.dampcake.bencode; 17 | 18 | import java.io.ByteArrayOutputStream; 19 | import java.io.FilterOutputStream; 20 | import java.io.IOException; 21 | import java.io.OutputStream; 22 | import java.nio.ByteBuffer; 23 | import java.nio.charset.Charset; 24 | import java.util.Map; 25 | import java.util.SortedMap; 26 | import java.util.TreeMap; 27 | 28 | /** 29 | * OutputStream for writing bencoded data. 30 | * 31 | * @author Adam Peck 32 | */ 33 | public class BencodeOutputStream extends FilterOutputStream { 34 | 35 | private final Charset charset; 36 | 37 | /** 38 | * Creates a new BencodeOutputStream that writes to the {@link OutputStream} passed and uses the {@link Charset} passed for encoding the data. 39 | * 40 | * @param out the {@link OutputStream} to write to 41 | * @param charset the {@link Charset} to use 42 | * 43 | * @throws NullPointerException if the {@link Charset} passed is null 44 | */ 45 | public BencodeOutputStream(final OutputStream out, final Charset charset) { 46 | super(out); 47 | 48 | if (charset == null) throw new NullPointerException("charset cannot be null"); 49 | this.charset = charset; 50 | } 51 | 52 | /** 53 | * Creates a new BencodeOutputStream that writes to the {@link OutputStream} passed and uses UTF-8 {@link Charset} for encoding the data. 54 | * 55 | * @param out the {@link OutputStream} to write to 56 | */ 57 | public BencodeOutputStream(final OutputStream out) { 58 | this(out, Bencode.DEFAULT_CHARSET); 59 | } 60 | 61 | /** 62 | * Gets the {@link Charset} the stream was created with. 63 | * 64 | * @return the {@link Charset} of the stream 65 | */ 66 | public Charset getCharset() { 67 | return charset; 68 | } 69 | 70 | /** 71 | * Writes the passed {@link String} to the stream. 72 | * 73 | * @param s the {@link String} to write to the stream 74 | * 75 | * @throws NullPointerException if the String is null 76 | * @throws IOException if the underlying stream throws 77 | */ 78 | public void writeString(final String s) throws IOException { 79 | write(encode(s)); 80 | } 81 | 82 | /** 83 | * Writes the passed {@link ByteBuffer} to the stream. 84 | * 85 | * @param buff the {@link ByteBuffer} to write to the stream 86 | * 87 | * @throws NullPointerException if the {@link ByteBuffer} is null 88 | * @throws IOException if the underlying stream throws 89 | * 90 | * @since 1.3 91 | */ 92 | public void writeString(final ByteBuffer buff) throws IOException { 93 | write(encode(buff.array())); 94 | } 95 | 96 | /** 97 | * Writes the passed byte[] to the stream. 98 | * 99 | * @param array the byte[] to write to the stream 100 | * 101 | * @throws NullPointerException if the byte[] is null 102 | * @throws IOException if the underlying stream throws 103 | * 104 | * @since 1.4.1 105 | */ 106 | public void writeString(final byte[] array) throws IOException { 107 | write(encode(array)); 108 | } 109 | 110 | /** 111 | * Writes the passed {@link Number} to the stream. 112 | *

113 | * The number is converted to a {@link Long}, meaning any precision is lost as it not supported by the bencode spec. 114 | *

115 | * 116 | * @param n the {@link Number} to write to the stream 117 | * 118 | * @throws NullPointerException if the {@link Number} is null 119 | * @throws IOException if the underlying stream throws 120 | */ 121 | public void writeNumber(final Number n) throws IOException { 122 | write(encode(n)); 123 | } 124 | 125 | /** 126 | * Writes the passed {@link Iterable} to the stream. 127 | *

128 | * Data contained in the {@link Iterable} is written as the correct type. Any {@link Iterable} is written as a List, 129 | * any {@link Number} as a Number, any {@link Map} as a Dictionary and any other {@link Object} is written as a String 130 | * calling the {@link Object#toString()} method. 131 | * 132 | * @param l the {@link Iterable} to write to the stream 133 | * 134 | * @throws NullPointerException if the {@link Iterable} is null 135 | * @throws IOException if the underlying stream throws 136 | */ 137 | public void writeList(final Iterable l) throws IOException { 138 | write(encode(l)); 139 | } 140 | 141 | /** 142 | * Writes the passed {@link Map} to the stream. 143 | *

144 | * Data contained in the {@link Map} is written as the correct type. Any {@link Iterable} is written as a List, 145 | * any {@link Number} as a Number, any {@link Map} as a Dictionary and any other {@link Object} is written as a String 146 | * calling the {@link Object#toString()} method. 147 | * 148 | * @param m the {@link Map} to write to the stream 149 | * 150 | * @throws NullPointerException if the {@link Map} is null 151 | * @throws IOException if the underlying stream throws 152 | */ 153 | public void writeDictionary(final Map m) throws IOException { 154 | write(encode(m)); 155 | } 156 | 157 | private byte[] encode(final String s) throws IOException { 158 | if (s == null) throw new NullPointerException("s cannot be null"); 159 | 160 | ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 161 | byte[] bytes = s.getBytes(charset); 162 | buffer.write(Integer.toString(bytes.length).getBytes(charset)); 163 | buffer.write(Bencode.SEPARATOR); 164 | buffer.write(bytes); 165 | 166 | return buffer.toByteArray(); 167 | } 168 | 169 | private byte[] encode(final byte[] b) throws IOException { 170 | if (b == null) throw new NullPointerException("b cannot be null"); 171 | 172 | ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 173 | 174 | buffer.write(Integer.toString(b.length).getBytes(charset)); 175 | buffer.write(Bencode.SEPARATOR); 176 | buffer.write(b); 177 | 178 | return buffer.toByteArray(); 179 | } 180 | 181 | private byte[] encode(final Number n) throws IOException { 182 | if (n == null) throw new NullPointerException("n cannot be null"); 183 | 184 | ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 185 | buffer.write(Bencode.NUMBER); 186 | buffer.write(Long.toString(n.longValue()).getBytes(charset)); 187 | buffer.write(Bencode.TERMINATOR); 188 | 189 | return buffer.toByteArray(); 190 | } 191 | 192 | private byte[] encode(final Iterable l) throws IOException { 193 | if (l == null) throw new NullPointerException("l cannot be null"); 194 | 195 | ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 196 | buffer.write(Bencode.LIST); 197 | for (Object o : l) 198 | buffer.write(encodeObject(o)); 199 | buffer.write(Bencode.TERMINATOR); 200 | 201 | return buffer.toByteArray(); 202 | } 203 | 204 | private byte[] encode(final Map m) throws IOException { 205 | if (m == null) throw new NullPointerException("m cannot be null"); 206 | 207 | Map map; 208 | if (!(m instanceof SortedMap)) 209 | map = new TreeMap<>(m); 210 | else 211 | map = m; 212 | 213 | ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 214 | buffer.write(Bencode.DICTIONARY); 215 | for (Map.Entry e : map.entrySet()) { 216 | buffer.write(encode(e.getKey().toString())); 217 | buffer.write(encodeObject(e.getValue())); 218 | } 219 | buffer.write(Bencode.TERMINATOR); 220 | 221 | return buffer.toByteArray(); 222 | } 223 | 224 | private byte[] encodeObject(final Object o) throws IOException { 225 | if (o == null) throw new NullPointerException("Cannot write null objects"); 226 | 227 | if (o instanceof Number) 228 | return encode((Number) o); 229 | if (o instanceof Iterable) 230 | return encode((Iterable) o); 231 | if (o instanceof Map) 232 | return encode((Map) o); 233 | if (o instanceof ByteBuffer) 234 | return encode(((ByteBuffer) o).array()); 235 | if (o instanceof byte[]) 236 | return encode((byte[]) o); 237 | 238 | return encode(o.toString()); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/main/java/com/dampcake/bencode/Bencode.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Adam Peck. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.dampcake.bencode; 17 | 18 | import java.io.ByteArrayInputStream; 19 | import java.io.ByteArrayOutputStream; 20 | import java.io.IOException; 21 | import java.nio.charset.Charset; 22 | import java.util.Map; 23 | 24 | /** 25 | * Bencode encoder/decoder. 26 | * 27 | * @author Adam Peck 28 | * @since 1.1 29 | */ 30 | public final class Bencode { 31 | 32 | /** Default Charset used by the Streams */ 33 | static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); 34 | 35 | /** Number Marker */ 36 | static final char NUMBER = 'i'; 37 | 38 | /** List Marker */ 39 | static final char LIST = 'l'; 40 | 41 | /** Dictionary Marker */ 42 | static final char DICTIONARY = 'd'; 43 | 44 | /** End of type Marker */ 45 | static final char TERMINATOR = 'e'; 46 | 47 | /** Separator between length and string */ 48 | static final char SEPARATOR = ':'; 49 | 50 | private final Charset charset; 51 | private final boolean useBytes; 52 | 53 | /** 54 | * Create a new Bencoder using the default {@link Charset} (UTF-8) and useBytes as false. 55 | * 56 | * @see #Bencode(Charset, boolean) 57 | */ 58 | public Bencode() { 59 | this(DEFAULT_CHARSET); 60 | } 61 | 62 | /** 63 | * Creates a new Bencoder using the {@link Charset} passed for encoding/decoding and useBytes as false. 64 | * 65 | * @param charset the {@link Charset} to use 66 | * 67 | * @throws NullPointerException if the {@link Charset} passed is null 68 | * 69 | * @see #Bencode(Charset, boolean) 70 | */ 71 | public Bencode(final Charset charset) { 72 | this(charset, false); 73 | } 74 | 75 | /** 76 | * Creates a new Bencoder using the boolean passed to control String parsing. 77 | * 78 | * @param useBytes {@link #Bencode(Charset, boolean)} 79 | * 80 | * @since 1.3 81 | */ 82 | public Bencode(final boolean useBytes) { 83 | this(DEFAULT_CHARSET, useBytes); 84 | } 85 | 86 | /** 87 | * Creates a new Bencoder using the {@link Charset} passed for encoding/decoding and boolean passed to control String parsing. 88 | * 89 | * If useBytes is false, then dictionary values that contain byte string data will be coerced to a {@link String}. 90 | * if useBytes is true, then dictionary values that contain byte string data will be coerced to a {@link java.nio.ByteBuffer}. 91 | * 92 | * @param charset the {@link Charset} to use 93 | * @param useBytes true to have dictionary byte data to stay as bytes 94 | * 95 | * @throws NullPointerException if the {@link Charset} passed is null 96 | * 97 | * @since 1.3 98 | */ 99 | public Bencode(final Charset charset, final boolean useBytes) { 100 | if (charset == null) throw new NullPointerException("charset cannot be null"); 101 | 102 | this.charset = charset; 103 | this.useBytes = useBytes; 104 | } 105 | 106 | /** 107 | * Gets the {@link Charset} the coder was created with. 108 | * 109 | * @return the {@link Charset} of the coder 110 | */ 111 | public Charset getCharset() { 112 | return charset; 113 | } 114 | 115 | /** 116 | * Determines the first {@link Type} contained within the byte array. 117 | * 118 | * @param bytes the bytes to determine the {@link Type} for 119 | * 120 | * @return the {@link Type} or {@link Type#UNKNOWN} if it cannot be determined 121 | * 122 | * @throws NullPointerException if bytes is null 123 | * @throws BencodeException if an error occurs during detection 124 | */ 125 | public Type type(final byte[] bytes) { 126 | if (bytes == null) throw new NullPointerException("bytes cannot be null"); 127 | 128 | try (BencodeInputStream in = new BencodeInputStream(new ByteArrayInputStream(bytes), charset, useBytes)) { 129 | return in.nextType(); 130 | } catch (Throwable t) { 131 | throw new BencodeException("Exception thrown during type detection", t); 132 | } 133 | } 134 | 135 | /** 136 | * Decodes a bencode encoded byte array. 137 | * 138 | * @param inferred from the {@link Type} parameter 139 | * @param bytes the bytes to decode 140 | * @param type the {@link Type} to decode as 141 | * 142 | * @return the decoded object 143 | * 144 | * @throws NullPointerException if bytes or type is null 145 | * @throws IllegalArgumentException if type is {@link Type#UNKNOWN} 146 | * @throws BencodeException if an error occurs during decoding 147 | */ 148 | @SuppressWarnings("unchecked") 149 | public T decode(final byte[] bytes, final Type type) { 150 | if (bytes == null) throw new NullPointerException("bytes cannot be null"); 151 | if (type == null) throw new NullPointerException("type cannot be null"); 152 | if (type == Type.UNKNOWN) throw new IllegalArgumentException("type cannot be UNKNOWN"); 153 | 154 | try (BencodeInputStream in = new BencodeInputStream(new ByteArrayInputStream(bytes), charset, useBytes)) { 155 | if (type == Type.NUMBER) 156 | return (T) in.readNumber(); 157 | if (type == Type.LIST) 158 | return (T) in.readList(); 159 | if (type == Type.DICTIONARY) 160 | return (T) in.readDictionary(); 161 | return (T) in.readString(); 162 | } catch (Throwable t) { 163 | throw new BencodeException("Exception thrown during decoding", t); 164 | } 165 | } 166 | 167 | /** 168 | * Encodes the passed {@link String}. 169 | * 170 | * @param s the {@link String} to encode 171 | * 172 | * @return the encoded bytes 173 | * 174 | * @throws NullPointerException if the {@link String} is null 175 | * @throws BencodeException if an error occurs during encoding 176 | */ 177 | public byte[] encode(final String s) { 178 | if (s == null) throw new NullPointerException("s cannot be null"); 179 | 180 | return encode(bencode -> bencode.writeString(s)); 181 | } 182 | 183 | /** 184 | * Encodes the passed {@link Number}. 185 | *

186 | * The number is converted to a {@link Long}, meaning any precision is lost as it not supported by the bencode spec. 187 | * 188 | * @param n the {@link Number} to encode 189 | * 190 | * @return the encoded bytes 191 | * 192 | * @throws NullPointerException if the {@link Number} is null 193 | * @throws BencodeException if an error occurs during encoding 194 | */ 195 | public byte[] encode(final Number n) { 196 | if (n == null) throw new NullPointerException("n cannot be null"); 197 | 198 | return encode(bencode -> bencode.writeNumber(n)); 199 | } 200 | 201 | /** 202 | * Encodes the passed {@link Iterable} as a bencode List. 203 | *

204 | * Data contained in the List is written as the correct type. Any {@link Iterable} is written as a List, 205 | * any {@link Number} as a Number, any {@link Map} as a Dictionary and any other {@link Object} is written as a String 206 | * calling the {@link Object#toString()} method. 207 | * 208 | * @param l the {@link Iterable} to encode 209 | * 210 | * @return the encoded bytes 211 | * 212 | * @throws NullPointerException if the List is null 213 | * @throws BencodeException if an error occurs during encoding 214 | */ 215 | public byte[] encode(final Iterable l) { 216 | if (l == null) throw new NullPointerException("l cannot be null"); 217 | 218 | return encode(bencode -> bencode.writeList(l)); 219 | } 220 | 221 | /** 222 | * Encodes the passed {@link Map} as a bencode Dictionary. 223 | *

224 | * Data contained in the Dictionary is written as the correct type. Any {@link Iterable} is written as a List, 225 | * any {@link Number} as a Number, any {@link Map} as a Dictionary and any other {@link Object} is written as a String 226 | * calling the {@link Object#toString()} method. 227 | * 228 | * @param m the {@link Map} to encode 229 | * 230 | * @return the encoded bytes 231 | * 232 | * @throws NullPointerException if the Map is null 233 | * @throws BencodeException if an error occurs during encoding 234 | */ 235 | public byte[] encode(final Map m) { 236 | if (m == null) throw new NullPointerException("m cannot be null"); 237 | 238 | return encode(bencode -> bencode.writeDictionary(m)); 239 | } 240 | 241 | private byte[] encode(final ThrowingConsumer function) { 242 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 243 | 244 | try (BencodeOutputStream bencode = new BencodeOutputStream(out, charset)) { 245 | function.accept(bencode); 246 | } catch (Throwable t) { 247 | throw new BencodeException("Exception thrown during encoding", t); 248 | } 249 | 250 | return out.toByteArray(); 251 | } 252 | 253 | @FunctionalInterface 254 | public interface ThrowingConsumer { 255 | public void accept(T t) throws IOException; 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/main/java/com/dampcake/bencode/BencodeInputStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Adam Peck. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.dampcake.bencode; 17 | 18 | import java.io.EOFException; 19 | import java.io.FilterInputStream; 20 | import java.io.IOException; 21 | import java.io.InputStream; 22 | import java.io.InvalidObjectException; 23 | import java.io.PushbackInputStream; 24 | import java.math.BigDecimal; 25 | import java.nio.ByteBuffer; 26 | import java.nio.charset.Charset; 27 | import java.util.ArrayList; 28 | import java.util.LinkedHashMap; 29 | import java.util.List; 30 | import java.util.Map; 31 | 32 | /** 33 | * InputStream for reading bencoded data. 34 | * 35 | * @author Adam Peck 36 | */ 37 | public class BencodeInputStream extends FilterInputStream { 38 | 39 | // EOF Constant 40 | private static final int EOF = -1; 41 | 42 | private final Charset charset; 43 | private final boolean useBytes; 44 | private final PushbackInputStream in; 45 | 46 | /** 47 | * Creates a new BencodeInputStream that reads from the {@link InputStream} passed and uses the {@link Charset} passed for decoding the data 48 | * and boolean passed to control String parsing. 49 | * 50 | * If useBytes is false, then dictionary values that contain byte string data will be coerced to a {@link String}. 51 | * if useBytes is true, then dictionary values that contain byte string data will be coerced to a {@link ByteBuffer}. 52 | * 53 | * @param in the {@link InputStream} to read from 54 | * @param charset the {@link Charset} to use 55 | * @param useBytes controls coercion of dictionary values 56 | * 57 | * @throws NullPointerException if the {@link Charset} passed is null 58 | * 59 | * @since 1.3 60 | */ 61 | public BencodeInputStream(final InputStream in, final Charset charset, boolean useBytes) { 62 | super(new PushbackInputStream(in)); 63 | this.in = (PushbackInputStream) super.in; 64 | 65 | if (charset == null) throw new NullPointerException("charset cannot be null"); 66 | this.charset = charset; 67 | this.useBytes = useBytes; 68 | } 69 | 70 | /** 71 | * Creates a new BencodeInputStream that reads from the {@link InputStream} passed and uses the {@link Charset} passed for decoding the data 72 | * and coerces dictionary values to a {@link String}. 73 | * 74 | * @param in the {@link InputStream} to read from 75 | * @param charset the {@link Charset} to use 76 | * 77 | * @throws NullPointerException if the {@link Charset} passed is null 78 | * 79 | * @see #BencodeInputStream(InputStream, Charset, boolean) 80 | */ 81 | public BencodeInputStream(final InputStream in, final Charset charset) { 82 | this(in, charset, false); 83 | } 84 | 85 | /** 86 | * Creates a new BencodeInputStream that reads from the {@link InputStream} passed and uses the UTF-8 {@link Charset} for decoding the data 87 | * and coerces dictionary values to a {@link String}. 88 | * 89 | * @param in the {@link InputStream} to read from 90 | * 91 | * @see #BencodeInputStream(InputStream, Charset, boolean) 92 | */ 93 | public BencodeInputStream(final InputStream in) { 94 | this(in, Bencode.DEFAULT_CHARSET); 95 | } 96 | 97 | /** 98 | * Gets the {@link Charset} the stream was created with. 99 | * 100 | * @return the {@link Charset} of the stream 101 | */ 102 | public Charset getCharset() { 103 | return charset; 104 | } 105 | 106 | private int peek() throws IOException { 107 | int b = in.read(); 108 | in.unread(b); 109 | return b; 110 | } 111 | 112 | /** 113 | * Peeks at the next {@link Type}. 114 | * 115 | * @return the next {@link Type} available 116 | * 117 | * @throws IOException if the underlying stream throws 118 | * @throws EOFException if the end of the stream has been reached 119 | */ 120 | public Type nextType() throws IOException { 121 | int token = peek(); 122 | checkEOF(token); 123 | 124 | return typeForToken(token); 125 | } 126 | 127 | private Type typeForToken(int token) { 128 | for (Type type : Type.values()) { 129 | if (type.validate(token)) 130 | return type; 131 | } 132 | 133 | return Type.UNKNOWN; 134 | } 135 | 136 | /** 137 | * Reads a {@link String} from the stream. 138 | * 139 | * @return the {@link String} read from the stream 140 | * 141 | * @throws IOException if the underlying stream throws 142 | * @throws EOFException if the end of the stream has been reached 143 | * @throws InvalidObjectException if the next type in the stream is not a String 144 | */ 145 | public String readString() throws IOException { 146 | return new String(readStringBytesInternal(), getCharset()); 147 | } 148 | 149 | /** 150 | * Reads a Byte String from the stream. 151 | * 152 | * @return the {@link ByteBuffer} read from the stream 153 | * 154 | * @throws IOException if the underlying stream throws 155 | * @throws EOFException if the end of the stream has been reached 156 | * @throws InvalidObjectException if the next type in the stream is not a String 157 | * 158 | * @since 1.3 159 | */ 160 | public ByteBuffer readStringBytes() throws IOException { 161 | return ByteBuffer.wrap(readStringBytesInternal()); 162 | } 163 | 164 | private byte[] readStringBytesInternal() throws IOException { 165 | int token = in.read(); 166 | validateToken(token, Type.STRING); 167 | 168 | StringBuilder buffer = new StringBuilder(); 169 | buffer.append((char) token); 170 | while ((token = in.read()) != Bencode.SEPARATOR) { 171 | validateToken(token, Type.STRING); 172 | 173 | buffer.append((char) token); 174 | } 175 | 176 | int length = Integer.parseInt(buffer.toString()); 177 | byte[] bytes = new byte[length]; 178 | read(bytes); 179 | return bytes; 180 | } 181 | 182 | /** 183 | * Reads a Number from the stream. 184 | * 185 | * @return the Number read from the stream 186 | * 187 | * @throws IOException if the underlying stream throws 188 | * @throws EOFException if the end of the stream has been reached 189 | * @throws InvalidObjectException if the next type in the stream is not a Number 190 | */ 191 | public Long readNumber() throws IOException { 192 | int token = in.read(); 193 | validateToken(token, Type.NUMBER); 194 | 195 | StringBuilder buffer = new StringBuilder(); 196 | while ((token = in.read()) != Bencode.TERMINATOR) { 197 | checkEOF(token); 198 | 199 | buffer.append((char) token); 200 | } 201 | 202 | return new BigDecimal(buffer.toString()).longValue(); 203 | } 204 | 205 | /** 206 | * Reads a List from the stream. 207 | * 208 | * @return the List read from the stream 209 | * 210 | * @throws IOException if the underlying stream throws 211 | * @throws EOFException if the end of the stream has been reached 212 | * @throws InvalidObjectException if the next type in the stream is not a List, or the list contains invalid types 213 | */ 214 | public List readList() throws IOException { 215 | int token = in.read(); 216 | validateToken(token, Type.LIST); 217 | 218 | List list = new ArrayList(); 219 | while ((token = in.read()) != Bencode.TERMINATOR) { 220 | checkEOF(token); 221 | 222 | list.add(readObject(token)); 223 | } 224 | 225 | return list; 226 | } 227 | 228 | /** 229 | * Reads a Dictionary from the stream. 230 | * 231 | * @return the Dictionary read from the stream 232 | * 233 | * @throws IOException if the underlying stream throws 234 | * @throws EOFException if the end of the stream has been reached 235 | * @throws InvalidObjectException if the next type in the stream is not a Dictionary, or the list contains invalid types 236 | */ 237 | public Map readDictionary() throws IOException { 238 | int token = in.read(); 239 | validateToken(token, Type.DICTIONARY); 240 | 241 | Map map = new LinkedHashMap(); 242 | while ((token = in.read()) != Bencode.TERMINATOR) { 243 | checkEOF(token); 244 | 245 | in.unread(token); 246 | map.put(readString(), readObject(in.read())); 247 | } 248 | 249 | return map; 250 | } 251 | 252 | private Object readObject(final int token) throws IOException { 253 | in.unread(token); 254 | 255 | Type type = typeForToken(token); 256 | 257 | if (type == Type.STRING && !useBytes) 258 | return readString(); 259 | if (type == Type.STRING) 260 | return readStringBytes(); 261 | if (type == Type.NUMBER) 262 | return readNumber(); 263 | if (type == Type.LIST) 264 | return readList(); 265 | if (type == Type.DICTIONARY) 266 | return readDictionary(); 267 | 268 | throw new InvalidObjectException("Unexpected token '" + new String(Character.toChars(token)) + "'"); 269 | } 270 | 271 | private void validateToken(final int token, final Type type) throws IOException { 272 | checkEOF(token); 273 | 274 | if (!type.validate(token)) { 275 | in.unread(token); 276 | throw new InvalidObjectException("Unexpected token '" + new String(Character.toChars(token)) + "'"); 277 | } 278 | } 279 | 280 | private void checkEOF(final int b) throws EOFException { 281 | if (b == EOF) throw new EOFException(); 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /src/test/java/com/dampcake/bencode/BencodeInputStreamTest.java: -------------------------------------------------------------------------------- 1 | package com.dampcake.bencode; 2 | 3 | import org.junit.Test; 4 | 5 | import java.io.ByteArrayInputStream; 6 | import java.io.EOFException; 7 | import java.io.InvalidObjectException; 8 | import java.nio.ByteBuffer; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | import static org.hamcrest.CoreMatchers.instanceOf; 13 | import static org.hamcrest.MatcherAssert.assertThat; 14 | import static org.junit.Assert.assertEquals; 15 | import static org.junit.Assert.assertThrows; 16 | import static org.junit.Assert.assertTrue; 17 | 18 | /** 19 | * Unit tests for BencodeInputStream 20 | * 21 | * @author Adam Peck 22 | */ 23 | public class BencodeInputStreamTest { 24 | 25 | private BencodeInputStream instance; 26 | 27 | private void instantiate(String s) { 28 | instantiate(s, false); 29 | } 30 | 31 | private void instantiate(String s, boolean useBytes) { 32 | instance = new BencodeInputStream( 33 | new ByteArrayInputStream(s.getBytes(Bencode.DEFAULT_CHARSET)), 34 | Bencode.DEFAULT_CHARSET, 35 | useBytes 36 | ); 37 | } 38 | 39 | @Test 40 | @SuppressWarnings("resource") 41 | public void testConstructorNullStream() throws Exception { 42 | new BencodeInputStream(null); 43 | } 44 | 45 | @Test(expected = NullPointerException.class) 46 | @SuppressWarnings("resource") 47 | public void testConstructorNullCharset() throws Exception { 48 | new BencodeInputStream(new ByteArrayInputStream(new byte[0]), null); 49 | } 50 | 51 | @Test 52 | public void testNextTypeString() throws Exception { 53 | instantiate("7"); 54 | 55 | assertEquals(Type.STRING, instance.nextType()); 56 | assertEquals(1, instance.available()); 57 | } 58 | 59 | @Test 60 | public void testNextTypeNumber() throws Exception { 61 | instantiate("i1"); 62 | 63 | assertEquals(Type.NUMBER, instance.nextType()); 64 | assertEquals(2, instance.available()); 65 | } 66 | 67 | @Test 68 | public void testNextTypeList() throws Exception { 69 | instantiate("l123"); 70 | 71 | assertEquals(Type.LIST, instance.nextType()); 72 | assertEquals(4, instance.available()); 73 | } 74 | 75 | @Test 76 | public void testNextTypeDictionary() throws Exception { 77 | instantiate("dtesting"); 78 | 79 | assertEquals(Type.DICTIONARY, instance.nextType()); 80 | assertEquals(8, instance.available()); 81 | } 82 | 83 | @Test 84 | public void testNextTypeUnknown() throws Exception { 85 | instantiate("unknown"); 86 | 87 | assertEquals(Type.UNKNOWN, instance.nextType()); 88 | assertEquals(7, instance.available()); 89 | } 90 | 91 | @Test 92 | public void testReadString() throws Exception { 93 | instantiate("12:Hello World!123"); 94 | 95 | assertEquals("Hello World!", instance.readString()); 96 | assertEquals(3, instance.available()); 97 | } 98 | 99 | @Test 100 | public void testReadStringEmpty() throws Exception { 101 | instantiate("0:123"); 102 | 103 | assertEquals("", instance.readString()); 104 | assertEquals(3, instance.available()); 105 | } 106 | 107 | @Test 108 | public void testReadStringNaN() throws Exception { 109 | instantiate("1c3:Testing"); 110 | 111 | assertThrows(InvalidObjectException.class, instance::readString); 112 | assertEquals(10, instance.available()); 113 | } 114 | 115 | @Test 116 | public void testReadStringEOF() throws Exception { 117 | instantiate("123456"); 118 | 119 | assertThrows(EOFException.class, instance::readString); 120 | assertEquals(0, instance.available()); 121 | } 122 | 123 | @Test 124 | public void testReadStringEmptyStream() throws Exception { 125 | instantiate(""); 126 | 127 | assertThrows(EOFException.class, instance::readString); 128 | assertEquals(0, instance.available()); 129 | } 130 | 131 | @Test 132 | public void testReadStringByteArray() throws Exception { 133 | instantiate("12:Hello World!123"); 134 | 135 | assertEquals("Hello World!", new String(instance.readStringBytes().array())); 136 | assertEquals(3, instance.available()); 137 | } 138 | 139 | @Test 140 | public void testReadStringEmptyByteArray() throws Exception { 141 | instantiate("0:123"); 142 | 143 | assertEquals("", new String(instance.readStringBytes().array())); 144 | assertEquals(3, instance.available()); 145 | } 146 | 147 | @Test 148 | public void testReadStringNaNByteArray() throws Exception { 149 | instantiate("1c3:Testing"); 150 | 151 | assertThrows(InvalidObjectException.class, instance::readStringBytes); 152 | assertEquals(10, instance.available()); 153 | } 154 | 155 | @Test 156 | public void testReadStringEOFByteArray() throws Exception { 157 | instantiate("123456"); 158 | 159 | assertThrows(EOFException.class, instance::readStringBytes); 160 | assertEquals(0, instance.available()); 161 | } 162 | 163 | @Test 164 | public void testReadStringEmptyStreamByteArray() throws Exception { 165 | instantiate(""); 166 | 167 | assertThrows(EOFException.class, instance::readStringBytes); 168 | assertEquals(0, instance.available()); 169 | } 170 | 171 | @Test 172 | public void testReadNumber() throws Exception { 173 | instantiate("i123456e123"); 174 | 175 | assertEquals(123456, instance.readNumber().longValue()); 176 | assertEquals(3, instance.available()); 177 | } 178 | 179 | @Test 180 | public void testReadNumberNaN() throws Exception { 181 | instantiate("i123cbve1"); 182 | 183 | assertThrows(NumberFormatException.class, instance::readNumber); 184 | assertEquals(1, instance.available()); 185 | } 186 | 187 | @Test 188 | public void testReadNumberEOF() throws Exception { 189 | instantiate("i123"); 190 | 191 | assertThrows(EOFException.class, instance::readNumber); 192 | assertEquals(0, instance.available()); 193 | } 194 | 195 | @Test 196 | public void testReadNumberEmptyStream() throws Exception { 197 | instantiate(""); 198 | 199 | assertThrows(EOFException.class, instance::readNumber); 200 | assertEquals(0, instance.available()); 201 | } 202 | 203 | @Test 204 | public void testReadNumberScientificNotation() throws Exception { 205 | instantiate("i-2.9155148901435E+18e"); 206 | 207 | assertEquals(-2915514890143500000L, instance.readNumber().longValue()); 208 | } 209 | 210 | @Test 211 | @SuppressWarnings("unchecked") 212 | public void testReadList() throws Exception { 213 | instantiate("l5:Hello6:World!li123ei456eeetesting"); 214 | 215 | List result = instance.readList(); 216 | 217 | assertEquals(3, result.size()); 218 | 219 | assertEquals("Hello", result.get(0)); 220 | assertEquals("World!", result.get(1)); 221 | 222 | List list = (List) result.get(2); 223 | assertEquals(123L, list.get(0)); 224 | assertEquals(456L, list.get(1)); 225 | 226 | assertEquals(7, instance.available()); 227 | } 228 | 229 | @Test 230 | @SuppressWarnings("unchecked") 231 | public void testReadListBytes() throws Exception { 232 | instantiate("l5:Hello6:World!li123ei456eeetesting", true); 233 | 234 | List result = instance.readList(); 235 | 236 | assertEquals(3, result.size()); 237 | 238 | assertThat(result.get(0), instanceOf(ByteBuffer.class)); 239 | assertEquals("Hello", new String(((ByteBuffer) result.get(0)).array())); 240 | assertThat(result.get(1), instanceOf(ByteBuffer.class)); 241 | assertEquals("World!", new String(((ByteBuffer) result.get(1)).array())); 242 | 243 | List list = (List) result.get(2); 244 | assertEquals(123L, list.get(0)); 245 | assertEquals(456L, list.get(1)); 246 | 247 | assertEquals(7, instance.available()); 248 | } 249 | 250 | @Test 251 | public void testReadListEmpty() throws Exception { 252 | instantiate("le123"); 253 | 254 | assertTrue(instance.readList().isEmpty()); 255 | assertEquals(3, instance.available()); 256 | } 257 | 258 | @Test 259 | public void testReadListInvalidItem() throws Exception { 260 | instantiate("l2:Worlde"); 261 | 262 | assertThrows(InvalidObjectException.class, instance::readList); 263 | assertEquals(4, instance.available()); 264 | } 265 | 266 | @Test 267 | public void testReadListEOF() throws Exception { 268 | instantiate("l5:Hello"); 269 | 270 | assertThrows(EOFException.class, instance::readList); 271 | assertEquals(0, instance.available()); 272 | } 273 | 274 | @Test 275 | @SuppressWarnings("unchecked") 276 | public void testReadDictionary() throws Exception { 277 | instantiate("d4:dictd3:1234:test3:4565:thinge4:listl11:list-item-111:list-item-2e6:numberi123456e6:string5:valuee"); 278 | 279 | Map result = instance.readDictionary(); 280 | 281 | assertEquals(4, result.size()); 282 | 283 | assertEquals("value", result.get("string")); 284 | assertEquals(123456L, result.get("number")); 285 | 286 | List list = (List) result.get("list"); 287 | assertEquals(2, list.size()); 288 | assertEquals("list-item-1", list.get(0)); 289 | assertEquals("list-item-2", list.get(1)); 290 | 291 | Map map = (Map) result.get("dict"); 292 | assertEquals(2, map.size()); 293 | assertEquals("test", map.get("123")); 294 | assertEquals("thing", map.get("456")); 295 | 296 | assertEquals(0, instance.available()); 297 | } 298 | 299 | @Test 300 | @SuppressWarnings("unchecked") 301 | public void testReadDictionaryByteArray() throws Exception { 302 | instantiate("d4:dictd3:1234:test3:4565:thinge4:listl11:list-item-111:list-item-2e6:numberi123456e6:string5:valuee", true); 303 | 304 | Map result = instance.readDictionary(); 305 | 306 | assertEquals(4, result.size()); 307 | 308 | assertThat(result.get("string"), instanceOf(ByteBuffer.class)); 309 | assertEquals("value", new String(((ByteBuffer) result.get("string")).array())); 310 | assertEquals(123456L, result.get("number")); 311 | 312 | List list = (List) result.get("list"); 313 | assertEquals(2, list.size()); 314 | assertThat(list.get(0), instanceOf(ByteBuffer.class)); 315 | assertEquals("list-item-1", new String(((ByteBuffer) list.get(0)).array())); 316 | assertThat(list.get(1), instanceOf(ByteBuffer.class)); 317 | assertEquals("list-item-2", new String(((ByteBuffer) list.get(1)).array())); 318 | 319 | Map map = (Map) result.get("dict"); 320 | assertEquals(2, map.size()); 321 | assertThat(map.get("123"), instanceOf(ByteBuffer.class)); 322 | assertEquals("test", new String(((ByteBuffer) map.get("123")).array())); 323 | assertThat(map.get("456"), instanceOf(ByteBuffer.class)); 324 | assertEquals("thing", new String(((ByteBuffer) map.get("456")).array())); 325 | 326 | assertEquals(0, instance.available()); 327 | } 328 | 329 | @Test 330 | public void testReadDictionaryEmpty() throws Exception { 331 | instantiate("de123test"); 332 | 333 | assertTrue(instance.readDictionary().isEmpty()); 334 | assertEquals(7, instance.available()); 335 | } 336 | 337 | @Test 338 | public void testReadDictionaryInvalidItem() throws Exception { 339 | instantiate("d4:item5:value3:testing"); 340 | 341 | assertThrows(InvalidObjectException.class, instance::readDictionary); 342 | assertEquals(4, instance.available()); 343 | } 344 | 345 | @Test 346 | public void testReadDictionaryEOF() throws Exception { 347 | instantiate("d4:item5:test"); 348 | 349 | assertThrows(EOFException.class, instance::readDictionary); 350 | assertEquals(0, instance.available()); 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /src/test/java/com/dampcake/bencode/BencodeTest.java: -------------------------------------------------------------------------------- 1 | package com.dampcake.bencode; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.junit.function.ThrowingRunnable; 6 | 7 | import java.io.EOFException; 8 | import java.io.InvalidObjectException; 9 | import java.nio.ByteBuffer; 10 | import java.nio.charset.Charset; 11 | import java.util.ArrayList; 12 | import java.util.HashMap; 13 | import java.util.LinkedHashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.TreeMap; 17 | import java.util.concurrent.ConcurrentSkipListMap; 18 | 19 | import static org.hamcrest.CoreMatchers.instanceOf; 20 | import static org.hamcrest.MatcherAssert.assertThat; 21 | import static org.junit.Assert.assertEquals; 22 | import static org.junit.Assert.assertSame; 23 | import static org.junit.Assert.assertThrows; 24 | import static org.junit.Assert.assertTrue; 25 | 26 | /** 27 | * Unit tests for Bencode. 28 | */ 29 | @SuppressWarnings("unchecked") 30 | public class BencodeTest { 31 | private Bencode instance; 32 | 33 | @Before 34 | public void setUp() { 35 | instance = new Bencode(); 36 | } 37 | 38 | @Test 39 | public void testConstructorNullCharset() { 40 | NullPointerException exception = assertThrows(NullPointerException.class, () -> new Bencode(null)); 41 | assertEquals("charset cannot be null", exception.getMessage()); 42 | } 43 | 44 | @Test 45 | public void testConstructorWithCharset() { 46 | Bencode bencode = new Bencode(Charset.forName("US-ASCII")); 47 | 48 | assertEquals(bencode.getCharset(), Charset.forName("US-ASCII")); 49 | } 50 | 51 | @Test 52 | public void testTypeString() { 53 | assertSame(Type.STRING, instance.type("7".getBytes())); 54 | } 55 | 56 | @Test 57 | public void testTypeNumber() { 58 | assertSame(Type.NUMBER, instance.type("i1".getBytes())); 59 | } 60 | 61 | @Test 62 | public void testTypeList() { 63 | assertSame(Type.LIST, instance.type("l123".getBytes())); 64 | } 65 | 66 | @Test 67 | public void testTypeDictionary() { 68 | assertSame(Type.DICTIONARY, instance.type("dtesting".getBytes())); 69 | } 70 | 71 | @Test 72 | public void testTypeUnknown() { 73 | assertSame(Type.UNKNOWN, instance.type("unknown".getBytes())); 74 | } 75 | 76 | @Test 77 | public void testTypeEmpty() { 78 | BencodeException exception = assertThrows(BencodeException.class, () -> instance.type(new byte[0])); 79 | assertThat(exception.getCause(), instanceOf(EOFException.class)); 80 | } 81 | 82 | @Test 83 | public void testTypeNullBytes() { 84 | NullPointerException exception = assertThrows(NullPointerException.class, () -> instance.type(null)); 85 | assertEquals("bytes cannot be null", exception.getMessage()); 86 | } 87 | 88 | @Test 89 | public void testDecodeNullBytes() { 90 | NullPointerException exception = assertThrows(NullPointerException.class, () -> instance.decode(null, Type.STRING)); 91 | assertEquals("bytes cannot be null", exception.getMessage()); 92 | } 93 | 94 | @Test 95 | public void testDecodeNullType() { 96 | NullPointerException exception = assertThrows(NullPointerException.class, () -> instance.decode("12:Hello World!".getBytes(), null)); 97 | assertEquals("type cannot be null", exception.getMessage()); 98 | } 99 | 100 | @Test 101 | public void testDecodeUnknownType() { 102 | IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> instance.decode("12:Hello World!".getBytes(), Type.UNKNOWN)); 103 | assertEquals("type cannot be UNKNOWN", exception.getMessage()); 104 | } 105 | 106 | @Test 107 | public void testDecodeString() { 108 | String decoded = instance.decode("12:Hello World!".getBytes(), Type.STRING); 109 | 110 | assertEquals("Hello World!", decoded); 111 | } 112 | 113 | @Test 114 | public void testDecodeStringMultiByteCodePoints() { 115 | String decoded = instance.decode("7:Garçon".getBytes(), Type.STRING); 116 | 117 | assertEquals("Garçon", decoded); 118 | } 119 | 120 | @Test 121 | public void testDecodeEmptyString() { 122 | String decoded = instance.decode("0:123".getBytes(), Type.STRING); 123 | 124 | assertEquals("", decoded); 125 | } 126 | 127 | @Test 128 | public void testDecodeStringNaN() throws Exception { 129 | BencodeException exception = assertThrows(BencodeException.class, () -> instance.decode("1c3:Testing".getBytes(), Type.STRING)); 130 | assertThat(exception.getCause(), instanceOf(InvalidObjectException.class)); 131 | } 132 | 133 | @Test 134 | public void testDecodeStringEOF() throws Exception { 135 | BencodeException exception = assertThrows(BencodeException.class, () -> instance.decode("123456".getBytes(), Type.STRING)); 136 | assertThat(exception.getCause(), instanceOf(EOFException.class)); 137 | } 138 | 139 | @Test 140 | public void testDecodeStringEmpty() throws Exception { 141 | BencodeException exception = assertThrows(BencodeException.class, () -> instance.decode("".getBytes(), Type.STRING)); 142 | assertThat(exception.getCause(), instanceOf(EOFException.class)); 143 | } 144 | 145 | @Test 146 | public void testDecodeNumber() throws Exception { 147 | long decoded = instance.decode("i123456e123".getBytes(), Type.NUMBER); 148 | 149 | assertEquals(123456, decoded); 150 | } 151 | 152 | @Test 153 | public void testDecodeNumberNaN() throws Exception { 154 | BencodeException exception = assertThrows(BencodeException.class, () -> instance.decode("i123cbve1".getBytes(), Type.NUMBER)); 155 | assertThat(exception.getCause(), instanceOf(NumberFormatException.class)); 156 | } 157 | 158 | @Test 159 | public void testDecodeNumberEOF() throws Exception { 160 | BencodeException exception = assertThrows(BencodeException.class, () -> instance.decode("i123".getBytes(), Type.NUMBER)); 161 | assertThat(exception.getCause(), instanceOf(EOFException.class)); 162 | } 163 | 164 | @Test 165 | public void testDecodeNumberEmpty() throws Exception { 166 | BencodeException exception = assertThrows(BencodeException.class, () -> instance.decode("".getBytes(), Type.NUMBER)); 167 | assertThat(exception.getCause(), instanceOf(EOFException.class)); 168 | } 169 | 170 | @Test 171 | public void testDecodeList() throws Exception { 172 | List decoded = instance.decode("l5:Hello6:World!li123ei456eeetesting".getBytes(), Type.LIST); 173 | 174 | assertEquals(3, decoded.size()); 175 | 176 | assertEquals("Hello", decoded.get(0)); 177 | assertEquals("World!", decoded.get(1)); 178 | 179 | List list = (List) decoded.get(2); 180 | assertEquals(123L, list.get(0)); 181 | assertEquals(456L, list.get(1)); 182 | } 183 | 184 | @Test 185 | public void testDecodeListByteArray() throws Exception { 186 | instance = new Bencode(true); 187 | List decoded = instance.decode("l5:Hello6:World!li123ei456eeetesting".getBytes(), Type.LIST); 188 | 189 | assertEquals(3, decoded.size()); 190 | 191 | assertThat(decoded.get(0), instanceOf(ByteBuffer.class)); 192 | assertEquals("Hello", new String(((ByteBuffer) decoded.get(0)).array())); 193 | assertThat(decoded.get(1), instanceOf(ByteBuffer.class)); 194 | assertEquals("World!", new String(((ByteBuffer) decoded.get(1)).array())); 195 | 196 | List list = (List) decoded.get(2); 197 | assertEquals(123L, list.get(0)); 198 | assertEquals(456L, list.get(1)); 199 | } 200 | 201 | @Test 202 | public void testDecodeListEmpty() throws Exception { 203 | List decoded = instance.decode("le123".getBytes(), Type.LIST); 204 | 205 | assertTrue(decoded.isEmpty()); 206 | } 207 | 208 | @Test 209 | public void testDecodeListInvalidItem() throws Exception { 210 | BencodeException exception = assertThrows(BencodeException.class, () -> instance.decode("l2:Worlde".getBytes(), Type.LIST)); 211 | assertThat(exception.getCause(), instanceOf(InvalidObjectException.class)); 212 | } 213 | 214 | @Test 215 | public void testDecodeListEOF() throws Exception { 216 | BencodeException exception = assertThrows(BencodeException.class, () -> instance.decode("l5:Hello".getBytes(), Type.LIST)); 217 | assertThat(exception.getCause(), instanceOf(EOFException.class)); 218 | } 219 | 220 | @Test 221 | public void testDecodeDictionary() throws Exception { 222 | Map decoded = instance.decode("d4:dictd3:1234:test3:4565:thinge4:listl11:list-item-111:list-item-2e6:numberi123456e6:string5:valuee".getBytes(), Type.DICTIONARY); 223 | 224 | assertEquals(4, decoded.size()); 225 | 226 | assertEquals("value", decoded.get("string")); 227 | assertEquals(123456L, decoded.get("number")); 228 | 229 | List list = (List) decoded.get("list"); 230 | assertEquals(2, list.size()); 231 | assertEquals("list-item-1", list.get(0)); 232 | assertEquals("list-item-2", list.get(1)); 233 | 234 | Map map = (Map) decoded.get("dict"); 235 | assertEquals(2, map.size()); 236 | assertEquals("test", map.get("123")); 237 | assertEquals("thing", map.get("456")); 238 | } 239 | 240 | @Test 241 | public void testDecodeDebianTracker() throws Exception { 242 | Map decoded = instance.decode("d8:intervali900e5:peersld2:ip12:146.71.73.514:porti63853eeee".getBytes(), Type.DICTIONARY); 243 | 244 | assertEquals(2, decoded.size()); 245 | 246 | assertEquals(900L, decoded.get("interval")); 247 | 248 | List list = (List) decoded.get("peers"); 249 | assertEquals(1, list.size()); 250 | 251 | Map map = (Map) list.get(0); 252 | assertEquals("146.71.73.51", map.get("ip")); 253 | assertEquals(63853L, map.get("port")); 254 | } 255 | 256 | @Test 257 | public void testDecodeDictionaryByteArray() throws Exception { 258 | instance = new Bencode(true); 259 | Map decoded = instance.decode("d4:dictd3:1234:test3:4565:thinge4:listl11:list-item-111:list-item-2e6:numberi123456e6:string5:valuee".getBytes(), Type.DICTIONARY); 260 | 261 | assertEquals(4, decoded.size()); 262 | 263 | assertThat(decoded.get("string"), instanceOf(ByteBuffer.class)); 264 | assertEquals("value", new String(((ByteBuffer) decoded.get("string")).array())); 265 | assertEquals(123456L, decoded.get("number")); 266 | 267 | List list = (List) decoded.get("list"); 268 | assertEquals(2, list.size()); 269 | assertThat(list.get(0), instanceOf(ByteBuffer.class)); 270 | assertEquals("list-item-1", new String(((ByteBuffer) list.get(0)).array())); 271 | assertThat(list.get(1), instanceOf(ByteBuffer.class)); 272 | assertEquals("list-item-2", new String(((ByteBuffer) list.get(1)).array())); 273 | 274 | Map map = (Map) decoded.get("dict"); 275 | assertEquals(2, map.size()); 276 | assertThat(map.get("123"), instanceOf(ByteBuffer.class)); 277 | assertEquals("test", new String(((ByteBuffer) map.get("123")).array())); 278 | assertThat(map.get("456"), instanceOf(ByteBuffer.class)); 279 | assertEquals("thing", new String(((ByteBuffer) map.get("456")).array())); 280 | } 281 | 282 | @Test 283 | public void testDecodeDictionaryEmpty() throws Exception { 284 | Map decoded = instance.decode("de123test".getBytes(), Type.DICTIONARY); 285 | 286 | assertTrue(decoded.isEmpty()); 287 | } 288 | 289 | @Test 290 | public void testDecodeDictionaryInvalidItem() throws Exception { 291 | BencodeException exception = assertThrows(BencodeException.class, () -> instance.decode("d4:item5:value3:testing".getBytes(), Type.DICTIONARY)); 292 | assertThat(exception.getCause(), instanceOf(InvalidObjectException.class)); 293 | } 294 | 295 | @Test 296 | public void testDecodeDictionaryEOF() throws Exception { 297 | BencodeException exception = assertThrows(BencodeException.class, () -> instance.decode("d4:item5:test".getBytes(), Type.DICTIONARY)); 298 | assertThat(exception.getCause(), instanceOf(EOFException.class)); 299 | } 300 | 301 | @Test 302 | public void testWriteString() throws Exception { 303 | byte[] encoded = instance.encode("Hello World!"); 304 | 305 | assertEquals("12:Hello World!", new String(encoded, instance.getCharset())); 306 | } 307 | 308 | @Test 309 | public void testWriteStringMultiByteCodePoints() throws Exception { 310 | byte[] encoded = instance.encode("Garçon"); 311 | 312 | assertEquals("7:Garçon", new String(encoded, instance.getCharset())); 313 | } 314 | @Test 315 | public void testWriteStringEmpty() throws Exception { 316 | byte[] encoded = instance.encode(""); 317 | 318 | assertEquals("0:", new String(encoded, instance.getCharset())); 319 | } 320 | 321 | @Test 322 | public void testWriteStringNull() throws Exception { 323 | NullPointerException exception = assertThrows(NullPointerException.class, () -> instance.encode((String) null)); 324 | assertEquals("s cannot be null", exception.getMessage()); 325 | } 326 | 327 | @Test 328 | public void testWriteNumber() throws Exception { 329 | byte[] encoded = instance.encode(123456); 330 | 331 | assertEquals("i123456e", new String(encoded, instance.getCharset())); 332 | } 333 | 334 | @Test 335 | public void testWriteNumberDecimal() throws Exception { 336 | byte[] encoded = instance.encode(123.456); 337 | 338 | assertEquals("i123e", new String(encoded, instance.getCharset())); 339 | } 340 | 341 | @Test 342 | public void testWriteNumberNull() throws Exception { 343 | NullPointerException exception = assertThrows(NullPointerException.class, () -> instance.encode((Number) null)); 344 | assertEquals("n cannot be null", exception.getMessage()); 345 | } 346 | 347 | @Test 348 | public void testWriteList() throws Exception { 349 | byte[] encoded = instance.encode(new ArrayList() {{ 350 | add("Hello"); 351 | add("World!"); 352 | add(new ArrayList() {{ 353 | add(123); 354 | add(456); 355 | }}); 356 | }}); 357 | 358 | assertEquals("l5:Hello6:World!li123ei456eee", new String(encoded, instance.getCharset())); 359 | } 360 | 361 | @Test 362 | public void testWriteListEmpty() throws Exception { 363 | byte[] encoded = instance.encode(new ArrayList()); 364 | 365 | assertEquals("le", new String(encoded, instance.getCharset())); 366 | } 367 | 368 | @Test 369 | public void testWriteListNullItem() throws Exception { 370 | ThrowingRunnable runnable = () -> instance.encode(new ArrayList() {{ 371 | add("Hello"); 372 | add("World!"); 373 | add(new ArrayList() {{ 374 | add(null); 375 | add(456); 376 | }}); 377 | }}); 378 | BencodeException exception = assertThrows(BencodeException.class, runnable); 379 | assertThat(exception.getCause(), instanceOf(NullPointerException.class)); 380 | } 381 | 382 | @Test 383 | public void testWriteListNull() throws Exception { 384 | NullPointerException exception = assertThrows(NullPointerException.class, () -> instance.encode((List) null)); 385 | assertEquals("l cannot be null", exception.getMessage()); 386 | } 387 | 388 | @Test 389 | public void testWriteDictionary() throws Exception { 390 | byte[] encoded = instance.encode(new LinkedHashMap() {{ 391 | put("string", "value"); 392 | put("number", 123456); 393 | put("list", new ArrayList() {{ 394 | add("list-item-1"); 395 | add("list-item-2"); 396 | }}); 397 | put("dict", new ConcurrentSkipListMap() {{ 398 | put(123, "test"); 399 | put(456, "thing"); 400 | }}); 401 | }}); 402 | 403 | assertEquals("d4:dictd3:1234:test3:4565:thinge4:listl11:list-item-111:list-item-2e6:numberi123456e6:string5:valuee", 404 | new String(encoded, instance.getCharset())); 405 | } 406 | 407 | @Test 408 | public void testWriteDictionaryEmpty() throws Exception { 409 | byte[] encoded = instance.encode(new HashMap()); 410 | 411 | assertEquals("de", new String(encoded, instance.getCharset())); 412 | } 413 | 414 | @Test 415 | public void testWriteDictionaryKeyCastException() throws Exception { 416 | ThrowingRunnable runnable = () -> instance.encode(new TreeMap() {{ 417 | put("string", "value"); 418 | put(123, "number-key"); 419 | }}); 420 | assertThrows(ClassCastException.class, runnable); 421 | } 422 | 423 | @Test 424 | public void testWriteDictionaryNull() throws Exception { 425 | NullPointerException exception = assertThrows(NullPointerException.class, () -> instance.encode((Map) null)); 426 | assertEquals("m cannot be null", exception.getMessage()); 427 | } 428 | } 429 | --------------------------------------------------------------------------------