package org.limewire.bittorrent.bencoding;
import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import org.limewire.util.BEncoder;
/**
* A bencoding Token that represents a string element of bencoded data.
* <p>
* In BitTorrent's bencoding, a string data on the wire looks like "5:hello".
* The length comes first, then a colon, then that number of characters.
* Bencoded strings hold ASCII text, or data of any format.
* <p>
* If bencoded data starts "0" through "9", it's a string.
* A BEString object can read the rest of it. Calling
* <code>beString.getToken()</code> returns a Java String with the payload text.
*/
class BEString extends Token<byte[]> {
/**
* The largest bencoded string we'll read.
*
* .torrent files don't have a maximum size, so for now this limit is
* set to 1 MB.
*
*/
//TODO: Find a proper way to deal with this limit.
private static final int MAX_STRING_SIZE = 1024 * 1024;
/**
* The first byte of the length of the string that was read
* from the channel, if any
*/
private final byte firstSizeByte;
/** Token that will be used to parse the string length. */
private BELong sizeToken;
/** The parsed length of the string. */
private int size = -1; // -1 because we haven't read the length prefix yet
/** Buffer used for internal storage. */
private ByteBuffer buf;
/** Empty Buffer to point a reference at. */
private static final ByteBuffer EMPTY_STRING = ByteBuffer.allocate(0);
/**
* Makes a new BEString Token ready to parse bencoded string data
* from a given ReadableByteChannel.
*
* @param firstChar the first byte we already read from the channel,
* it was "0" through "9" indicating this is a string
* @param chan the ReadableByteChannel the caller read the first
* character from, and we can read the remaining characters from
*/
BEString(byte firstChar, ReadableByteChannel chan) {
super(chan);
this.firstSizeByte = firstChar;
}
@Override
public void handleRead() throws IOException {
// If we haven't read the whole length prefix yet,
// try to read more
if (size == -1 && !readSize())
return; // Don't do more until the next read notification
if (size == 0)
return;
if (!buf.hasRemaining())
throw new IllegalStateException("Token is done - don't read to it");
int read = 0;
while (buf.hasRemaining() && (read = chan.read(buf)) > 0);
if (read == -1 && buf.hasRemaining())
throw new EOFException("Could not read String token");
}
/**
* Reads the length prefix at the start of a bencoded string.
*
* @return true if it got it all and set size, false to call
* it again to keep reading more
*/
private boolean readSize() throws IOException {
/*
* A bencoded string is like "17:this is the text".
* The length of "this is the text", 17, is written at the start
* before the colon.
*
* The first step is to read the length.
* To do this, readSize() makes a new BELong object.
* We give it our channel to read from and tell it to stop when it
* gets to a ":".
*
* We call handleRead() on it to get it to read bencoded data from
* our channel. When it's read the ":", it returns the number it
* read and parsed as a Long object, and control enters the if
* statement.
*/
if (sizeToken == null)
sizeToken = new BELong(chan, BEncoder.COLON, firstSizeByte);
sizeToken.handleRead();
Long l = sizeToken.getResult();
if (l != null) { // Same as size == -1
sizeToken = null; // We don't need the object anymore
long l2 = l.longValue();
// Valid length
if (l2 > 0 && l2 < MAX_STRING_SIZE) {
size = (int)l2;
result = new byte[size];
buf = ByteBuffer.wrap(result);
return true;
// The bencoded string data is "0:", this is valid
} else if (l2 == 0) {
size = 0;
buf = EMPTY_STRING;
result = new byte[0];
return true;
} else
throw new IOException("invalid string length"); // Too big
} else
return false; // We're still reading the size, try again next time
}
/**
* Determines if this is finished reading bencoded data from its channel
* and parsing it into a String object.
*
* @return true if it is done, false if it needs more read notifications to
* finish
*/
@Override
protected boolean isDone() {
// We're only done if we parsed the size to make a buffer that big,
// and then filled it
return buf != null && !buf.hasRemaining();
}
/**
* Hints that this BEString Token object is parsing a bencoded string
* into a Java byte [].
*
* @return Token.STRING
*/
@Override
public int getType() {
return STRING;
}
}