package org.klomp.snark;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import net.i2p.data.DataHelper;
import net.i2p.util.RandomSource;
import org.klomp.snark.bencode.BDecoder;
import org.klomp.snark.bencode.BEValue;
/**
* Simple state for the download of the metainfo, shared between
* Peer and ExtensionHandler.
*
* Nothing is synchronized here!
* Caller must synchronize on this for everything!
*
* Reference: BEP 9
*
* @since 0.8.4
* author zzz
*/
class MagnetState {
public static final int CHUNK_SIZE = 16*1024;
private final byte[] infohash;
private boolean complete;
/** if false, nothing below is valid */
private boolean isInitialized;
private int metaSize;
private int totalChunks;
/** bitfield for the metainfo chunks - will remain null if we start out complete */
private BitField requested;
private BitField have;
/** bitfield for the metainfo */
private byte[] metainfoBytes;
/** only valid when finished */
private MetaInfo metainfo;
/**
* @param meta null for new magnet
*/
public MagnetState(byte[] iHash, MetaInfo meta) {
infohash = iHash;
if (meta != null) {
metainfo = meta;
initialize(meta.getInfoBytes().length);
complete = true;
}
}
/**
* Call this for a new magnet when you have the size
* @throws IllegalArgumentException
*/
public void initialize(int size) {
if (isInitialized)
throw new IllegalArgumentException("already set");
isInitialized = true;
metaSize = size;
totalChunks = (size + (CHUNK_SIZE - 1)) / CHUNK_SIZE;
if (metainfo != null) {
metainfoBytes = metainfo.getInfoBytes();
} else {
// we don't need these if complete
have = new BitField(totalChunks);
requested = new BitField(totalChunks);
metainfoBytes = new byte[metaSize];
}
}
/**
* Call this for a new magnet when the download is complete.
* @throws IllegalArgumentException
*/
public void setMetaInfo(MetaInfo meta) {
metainfo = meta;
}
/**
* @throws IllegalArgumentException
*/
public MetaInfo getMetaInfo() {
if (!complete)
throw new IllegalArgumentException("not complete");
return metainfo;
}
/**
* @throws IllegalArgumentException
*/
public int getSize() {
if (!isInitialized)
throw new IllegalArgumentException("not initialized");
return metaSize;
}
public boolean isInitialized() {
return isInitialized;
}
public boolean isComplete() {
return complete;
}
public int chunkSize(int chunk) {
return Math.min(CHUNK_SIZE, metaSize - (chunk * CHUNK_SIZE));
}
/** @return chunk count */
public int chunksRemaining() {
if (!isInitialized)
throw new IllegalArgumentException("not initialized");
if (complete)
return 0;
return totalChunks - have.count();
}
/** @return chunk number */
public int getNextRequest() {
if (!isInitialized)
throw new IllegalArgumentException("not initialized");
if (complete)
throw new IllegalArgumentException("complete");
int rand = RandomSource.getInstance().nextInt(totalChunks);
for (int i = 0; i < totalChunks; i++) {
int chk = (i + rand) % totalChunks;
if (!(have.get(chk) || requested.get(chk))) {
requested.set(chk);
return chk;
}
}
// all requested - end game
for (int i = 0; i < totalChunks; i++) {
int chk = (i + rand) % totalChunks;
if (!have.get(chk))
return chk;
}
throw new IllegalArgumentException("complete");
}
/**
* @throws IllegalArgumentException
*/
public byte[] getChunk(int chunk) {
if (!complete)
throw new IllegalArgumentException("not complete");
if (chunk < 0 || chunk >= totalChunks)
throw new IllegalArgumentException("bad chunk number");
int size = chunkSize(chunk);
byte[] rv = new byte[size];
System.arraycopy(metainfoBytes, chunk * CHUNK_SIZE, rv, 0, size);
// use meta.getInfoBytes() so we don't save it in memory
return rv;
}
/**
* @return true if this was the last piece
* @throws NullPointerException IllegalArgumentException, IOException, ...
*/
public boolean saveChunk(int chunk, byte[] data, int off, int length) throws Exception {
if (!isInitialized)
throw new IllegalArgumentException("not initialized");
if (chunk < 0 || chunk >= totalChunks)
throw new IllegalArgumentException("bad chunk number");
if (have.get(chunk))
return false; // shouldn't happen if synced
int size = chunkSize(chunk);
if (size != length)
throw new IllegalArgumentException("bad chunk length");
System.arraycopy(data, off, metainfoBytes, chunk * CHUNK_SIZE, size);
have.set(chunk);
boolean done = have.complete();
if (done) {
metainfo = buildMetaInfo();
complete = true;
}
return done;
}
/**
* @return true if this was the last piece
* @throws NullPointerException IllegalArgumentException, IOException, ...
*/
private MetaInfo buildMetaInfo() throws Exception {
// top map has nothing in it but the info map (no announce)
Map<String, BEValue> map = new HashMap<String, BEValue>();
InputStream is = new ByteArrayInputStream(metainfoBytes);
BDecoder dec = new BDecoder(is);
BEValue bev = dec.bdecodeMap();
map.put("info", bev);
MetaInfo newmeta = new MetaInfo(map);
if (!DataHelper.eq(newmeta.getInfoHash(), infohash)) {
// Disaster. Start over. ExtensionHandler will catch
// the IOE and disconnect the peer, hopefully we will
// find a new peer.
// TODO: Count fails and give up eventually
have = new BitField(totalChunks);
requested = new BitField(totalChunks);
throw new IOException("info hash mismatch");
}
return newmeta;
}
}