Refactor bencode / torrent parser

This commit is contained in:
Reinhard Pointner 2019-02-20 02:09:51 +07:00
parent a63556e3f4
commit 3f3643ad71
4 changed files with 47 additions and 289 deletions

View File

@ -27,6 +27,7 @@
<dependency rev="9.20-2.00beta" org="net.sf.sevenzipjbinding" name="sevenzipjbinding" />
<dependency rev="9.20-2.00beta" org="net.sf.sevenzipjbinding" name="sevenzipjbinding-all-platforms" />
<dependency rev="0.6" org="com.optimaize.languagedetector" name="language-detector" />
<dependency rev="1.2.1" org="com.dampcake" name="bencode" />
<dependency rev="0.6.7" org="one.util" name="streamex" />
<dependency rev="3.0.1" org="com.googlecode.lanterna" name="lanterna" />
<dependency rev="2.11.0" org="com.drewnoakes" name="metadata-extractor" />

View File

@ -17,6 +17,7 @@ ivy/jar/junrar.jar
ivy/jar/jna.jar
ivy/jar/jna-platform.jar
ivy/jar/simmetrics-core.jar
ivy/jar/bencode.jar
ivy/jar/streamex.jar
ivy/jar/icu4j.jar
ivy/jar/language-detector.jar

View File

@ -1,217 +0,0 @@
/*
* BeDecoder.java
*
* Created on May 30, 2003, 2:44 PM
* Copyright (C) 2003, 2004, 2005, 2006 Aelitis, All Rights Reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
* AELITIS, SAS au capital de 46,603.30 euros
* 8 Allee Lenotre, La Grille Royale, 78600 Le Mesnil le Roi, France.
*/
package net.filebot.torrent;
import static java.nio.charset.StandardCharsets.*;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A set of utility methods to decode a bencoded array of byte into a Map. integer are represented as Long, String as byte[], dictionnaries as Map, and list as List.
*
* @author TdC_VgA
*/
class BDecoder {
public static Map<?, ?> decode(InputStream is) throws IOException {
return (new BDecoder().decodeStream(is));
}
public Map<?, ?> decodeStream(InputStream data) throws IOException {
Object res = decodeInputStream(data, 0);
if (res == null)
throw (new IOException("BDecoder: zero length file"));
else if (!(res instanceof Map))
throw (new IOException("BDecoder: top level isn't a Map"));
return ((Map<?, ?>) res);
}
private Object decodeInputStream(InputStream bais, int nesting) throws IOException {
if (!bais.markSupported())
throw new IOException("InputStream must support the mark() method");
// set a mark
bais.mark(Integer.MAX_VALUE);
// read a byte
int tempByte = bais.read();
// decide what to do
switch (tempByte) {
case 'd':
// create a new dictionary object
Map<String, Object> tempMap = new HashMap<String, Object>();
// get the key
byte[] tempByteArray = null;
while ((tempByteArray = (byte[]) decodeInputStream(bais, nesting + 1)) != null) {
// decode some more
Object value = decodeInputStream(bais, nesting + 1);
// add the value to the map
CharBuffer cb = ISO_8859_1.decode(ByteBuffer.wrap(tempByteArray));
String key = new String(cb.array(), 0, cb.limit());
tempMap.put(key, value);
}
if (bais.available() < nesting)
throw (new IOException("BDecoder: invalid input data, 'e' missing from end of dictionary"));
// return the map
return tempMap;
case 'l':
// create the list
List<Object> tempList = new ArrayList<Object>();
// create the key
Object tempElement = null;
while ((tempElement = decodeInputStream(bais, nesting + 1)) != null)
// add the element
tempList.add(tempElement);
if (bais.available() < nesting)
throw (new IOException("BDecoder: invalid input data, 'e' missing from end of list"));
// return the list
return tempList;
case 'e':
case -1:
return null;
case 'i':
return getNumberFromStream(bais, 'e');
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
// move back one
bais.reset();
// get the string
return getByteArrayFromStream(bais);
default: {
int rem_len = bais.available();
if (rem_len > 256)
rem_len = 256;
byte[] rem_data = new byte[rem_len];
bais.read(rem_data);
throw (new IOException("BDecoder: unknown command '" + tempByte + ", remainder = " + new String(rem_data)));
}
}
}
private long getNumberFromStream(InputStream bais, char parseChar) throws IOException {
int length = 0;
// place a mark
bais.mark(Integer.MAX_VALUE);
int tempByte = bais.read();
while ((tempByte != parseChar) && (tempByte >= 0)) {
tempByte = bais.read();
length++;
}
// are we at the end of the stream?
if (tempByte < 0)
return -1;
// reset the mark
bais.reset();
// get the length
byte[] tempArray = new byte[length];
int count = 0;
int len = 0;
// get the string
while ((count != length) && ((len = bais.read(tempArray, count, length - count)) > 0))
count += len;
// jump ahead in the stream to compensate for the :
bais.skip(1);
// return the value
CharBuffer cb = ISO_8859_1.decode(ByteBuffer.wrap(tempArray));
String str_value = new String(cb.array(), 0, cb.limit());
return Long.parseLong(str_value);
}
private byte[] getByteArrayFromStream(InputStream bais) throws IOException {
int length = (int) getNumberFromStream(bais, ':');
if (length < 0)
return null;
// note that torrent hashes can be big (consider a 55GB file with 2MB
// pieces
// this generates a pieces hash of 1/2 meg
if (length > 8 * 1024 * 1024)
throw (new IOException("Byte array length too large (" + length + ")"));
byte[] tempArray = new byte[length];
int count = 0;
int len = 0;
// get the string
while ((count != length) && ((len = bais.read(tempArray, count, length - count)) > 0))
count += len;
if (count != tempArray.length)
throw (new IOException("BDecoder::getByteArrayFromStream: truncated"));
return tempArray;
}
}

View File

@ -1,19 +1,17 @@
package net.filebot.torrent;
import static java.nio.charset.StandardCharsets.*;
import static java.util.Collections.*;
import static java.util.stream.Collectors.*;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Iterator;
import java.nio.file.Files;
import java.util.List;
import java.util.Map;
import com.dampcake.bencode.Bencode;
import com.dampcake.bencode.Type;
import net.filebot.vfs.FileInfo;
import net.filebot.vfs.SimpleFileInfo;
@ -39,87 +37,62 @@ public class Torrent {
}
public Torrent(Map<?, ?> torrentMap) {
Charset charset = UTF_8;
encoding = decodeString(torrentMap.get("encoding"), charset);
createdBy = getString(torrentMap.get("created by"));
announce = getString(torrentMap.get("announce"));
comment = getString(torrentMap.get("comment"));
creationDate = getLong(torrentMap.get("creation date"));
try {
charset = Charset.forName(encoding);
} catch (IllegalArgumentException e) {
// invalid encoding, just keep using UTF-8
}
Map<?, ?> info = getMap(torrentMap.get("info"));
name = getString(info.get("name"));
pieceLength = getLong(info.get("piece length"));
createdBy = decodeString(torrentMap.get("created by"), charset);
announce = decodeString(torrentMap.get("announce"), charset);
comment = decodeString(torrentMap.get("comment"), charset);
creationDate = decodeLong(torrentMap.get("creation date"));
Map<?, ?> infoMap = (Map<?, ?>) torrentMap.get("info");
name = decodeString(infoMap.get("name"), charset);
pieceLength = (Long) infoMap.get("piece length");
if (infoMap.containsKey("files")) {
if (info.containsKey("files")) {
// torrent contains multiple entries
singleFileTorrent = false;
List<FileInfo> entries = new ArrayList<FileInfo>();
for (Object fileMapObject : (List<?>) infoMap.get("files")) {
Map<?, ?> fileMap = (Map<?, ?>) fileMapObject;
List<?> pathList = (List<?>) fileMap.get("path");
StringBuilder path = new StringBuilder(80);
Iterator<?> iterator = pathList.iterator();
while (iterator.hasNext()) {
// append path element
path.append(decodeString(iterator.next(), charset));
// append separator
if (iterator.hasNext()) {
path.append("/");
}
}
Long length = decodeLong(fileMap.get("length"));
entries.add(new SimpleFileInfo(path.toString(), length));
}
files = unmodifiableList(entries);
files = getList(info.get("files")).stream().map(this::getMap).map(f -> {
String path = getList(f.get("path")).stream().map(Object::toString).collect(joining("/"));
long length = getLong(f.get("length"));
return new SimpleFileInfo(path, length);
}).collect(toList());
} else {
// single file torrent
// torrent contains only a single entry
singleFileTorrent = true;
Long length = decodeLong(infoMap.get("length"));
files = singletonList(new SimpleFileInfo(name, length));
files = singletonList(new SimpleFileInfo(name, getLong(info.get("length"))));
}
}
private static Map<?, ?> decodeTorrent(File torrent) throws IOException {
InputStream in = new BufferedInputStream(new FileInputStream(torrent));
byte[] bytes = Files.readAllBytes(torrent.toPath());
try {
return BDecoder.decode(in);
} finally {
in.close();
return new Bencode().decode(bytes, Type.DICTIONARY);
}
private String getString(Object value) {
if (value instanceof CharSequence) {
return value.toString();
}
return "";
}
private String decodeString(Object byteArray, Charset charset) {
if (byteArray == null)
return null;
return new String((byte[]) byteArray, charset);
private long getLong(Object value) {
if (value instanceof Number) {
return ((Number) value).longValue();
}
return -1;
}
private Long decodeLong(Object number) {
if (number == null)
return null;
private Map<?, ?> getMap(Object value) {
if (value instanceof Map) {
return (Map) value;
}
return emptyMap();
}
return (Long) number;
private List<?> getList(Object value) {
if (value instanceof List) {
return (List) value;
}
return emptyList();
}
public String getAnnounce() {
@ -143,7 +116,7 @@ public class Torrent {
}
public List<FileInfo> getFiles() {
return files;
return unmodifiableList(files);
}
public String getName() {