// Commented for the Learning branch
package com.limegroup.bittorrent.bencoding;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
/**
* A BEString object reads bencoded data like "5:hello" from a channel and parses it into a String object.
*
* In BitTorrent's bencoding, a string data on the wire looks like "23:this is the string data".
* The length comes first, then a colon, then that number of characters.
* Bencoded strings hold ASCII text, or data of any format.
*
* If bencoded data starts "0" through "9", it's a string.
* Make a new BEString object, giving it the digit you read and the channel where it can get the rest of them.
* It will use an internal BELong object to parse the length, make a ByteBuffer exactly the right size, and move the string data across.
* Call handleRead() when you know there's more bencoded data in the channel.
* When isDone() returns true, call getResult() to get the String with the data.
*/
class BEString extends Token {
/** 1 MB of bytes, the largest bencoded string we'll read. */
private static final int MAX_STRING_SIZE = 1024 * 1024;
/**
* The first numeral in the length of the string.
* We may have already read this from the channel to determine what kind of bencoded data is next in the channel.
*/
private final byte firstSizeByte;
/** A BELong object we'll use to parse the length of the string at the start of the bencoded data. */
private BELong sizeToken;
/** The length of the string. */
private int size = -1; // -1 because we haven't read the length prefix yet
/** The ByteBuffer handleRead() will use to hold all the string data. */
private ByteBuffer buf;
/** An empty ByteBuffer to reference when the bencoded data is "0:", for the empty string. */
private static final ByteBuffer EMPTY_STRING = ByteBuffer.allocate(0);
/** ":", A colon separates the length from the string, like "5:hello". */
final static byte COLON;
// Java runs this before control enters the first method here
static {
// Set COLON to the ASCII byte of the ":" character
byte colon = 0;
try { colon = ":".getBytes(ASCII)[0]; } catch (UnsupportedEncodingException impossible) {}
COLON = colon;
}
/**
* Make a new BEString object ready to read and parse a bencoded string like "5:hello" into a Java String like "hello".
*
* @param firstChar The first character of the bencoded string.
* It's a numeral, "0" through "9".
* We read it from the channel, and it told us this is a bencoded string.
* @param chan The channel we're reading bencoded data from.
*/
BEString(byte firstChar, ReadableByteChannel chan) {
// Save the given channel in this new object
super(chan);
// Save the first size byte the caller already read from the channel
this.firstSizeByte = firstChar; // We'd stuff it back into the channel if we could
}
/**
* The "NIODispatch" thread will call this handleRead() method when this BEString object can read more bencoded data from the channel we gave it.
*
* handleRead() calls readSize(), which parses the length prefix like "123:", sets the size member variable, and makes a buffer exactly the right size.
* It calls chan.read(buf), which copies data from the channel into buf.
* When buf is full, isDone() will start returning true, and getResult() will return the whole String.
*/
public void handleRead() throws IOException {
// If we haven't read the whole length prefix like "123:" yet, try to read more
if (size == -1 && // If we don't know how long the string is yet
!readSize()) // Call readSize() to read the length prefix, which returns true when it reads the ":" and sets size
return; // If readSize() didn't finish, try calling it again the next time
// readSize() read "0:", the complete bencoded string for blank, we're done
if (size == 0) return;
// No one should call handleRead() after we've read our whole string from the channel
if (!buf.hasRemaining()) throw new IllegalStateException("Token is done - don't read to it");
// Move data from the channel into buf until buf is full and we're done, or the channel doesn't have any more data right now
int read = 0;
while (buf.hasRemaining() && // readSize() made buf exactly the right size, if our buffer still has space
(read = chan.read(buf)) > 0) ; // Move data from the channel to the buffer, if we got some, loop to do it again
// Throw an exception if the channel closed before we could read the whole string
if (read == -1 && // If channel.read() returned -1, indicating the connection it represents is lost, and
buf.hasRemaining()) // Our buffer still has space, we're not done reading the whole thing
throw new IOException("closed before end of String token");
}
/**
* Read the length prefix at the start of a bencoded string, like "123:".
* Doesn't read any further than that.
* Turns the numerals into a number, and saves it in the size member variable.
* Sizes result and buf to hold the string data.
*
* @return True if we've read the whole length prefix, and are ready to read the string data.
* False if handlRead() should call readSize() some more so we can keep reading numerals to reach the ":".
*/
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.
*/
// The first time this runs, make a BELong object that will read numerals until it gets to a ":"
if (sizeToken == null) sizeToken = new BELong(chan, COLON, firstSizeByte); // Give it the first numeral we already read
// Tell our numeral reader to read more
sizeToken.handleRead(); // It will read up to the ":", but no further
// Find out if our numeral reader has read the whole length yet
Long parsedLong = (Long)sizeToken.getResult(); // If it's read all the numerals and the ":", getResult() will return a Long with the number
if (parsedLong != null) { // It's done, we set size to something other than -1 here, so handleRead() won't call readSize() again
// Free the numeral reader object
sizeToken = null; // We don't need the object anymore
long length = parsedLong.longValue();
// Valid length
if (length > 0 && length < MAX_STRING_SIZE) {
// Set the member variables to read the string data next
size = (int)length; // Now we know how long the data that comes next is
result = new byte[size]; // Make a byte array that can hold it exactly
buf = ByteBuffer.wrap((byte[])result); // Wrap a ByteBuffer around it to use it that way also
// Return true to indicate we finished reading the length prefix
return true;
// The bencoded string data is "0:", this is valid
} else if (length == 0) {
// Set the member variables of this BEString object to represent an empty String
size = 0; // No length
buf = EMPTY_STRING; // Point buf at the empty ByteBuffer we made
result = new byte[0]; // Have getResult() return an empty byte array
// Return true to indicate we finished reading the length prefix
return true;
// The length is negative, or too big
} else {
// Throw an exception instead of continuing
throw new IOException("invalid string length");
}
// sizeToken hasn't finished reading the length prefix like "123:" yet
} else {
// Return false to indicate we're still reading the length prefix, and handleRead() should call readSize() again
return false;
}
}
/**
* Find out if this BEString object is finished reading its bencoded string from its channel, and has parsed it into a String object.
*
* @return True, this object has read the entire bencoded string from the channel and getResult() will return a String.
* False, call the handleRead() method more to get this object to read more bencoded data from its channel.
*/
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();
}
/**
* Determine what kind of Token object this is, and what kind of Java object it will parse.
*
* @return Token.STRING, the code number for a BEString object that will produce a Java String
*/
public int getType() {
// Return the Token.STRING code
return STRING;
}
}