/*
* MetaInfo - Holds all information gotten from a torrent file. Copyright (C)
* 2003 Mark J. Wielaard
*
* This file is part of Snark.
*
* 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, 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.
*/
package org.klomp.snark;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.klomp.snark.bencode.BDecoder;
import org.klomp.snark.bencode.BEValue;
import org.klomp.snark.bencode.BEncoder;
import org.klomp.snark.bencode.InvalidBEncodingException;
public class MetaInfo
{
private final String announce;
private List<String> announceList;
private final byte[] info_hash;
private final String name;
private final List<List<String>> files;
private final List<Long> lengths;
private final int piece_length;
private final byte[] piece_hashes;
private final long length;
private byte[] torrentdata;
MetaInfo (String announce, List<String> announceList, String name,
List<List<String>> files,
List<Long> lengths, int piece_length, byte[] piece_hashes, long length)
{
this.announce = announce;
this.announceList = announceList;
this.name = name;
this.files = files;
this.lengths = lengths;
this.piece_length = piece_length;
this.piece_hashes = piece_hashes;
this.length = length;
this.info_hash = calculateInfoHash();
}
/**
* Creates a new MetaInfo from the given InputStream. The InputStream must
* start with a correctly bencoded dictonary describing the torrent.
*/
public MetaInfo (InputStream in) throws IOException
{
this(new BDecoder(in));
}
/**
* Creates a new MetaInfo from the given BDecoder. The BDecoder must have a
* complete dictionary describing the torrent.
*/
public MetaInfo (BDecoder be) throws IOException
{
// Note that evaluation order matters here...
this(be.bdecodeMap().getMap());
}
/**
* Creates a new MetaInfo from a Map of BEValues and the SHA1 over the
* original bencoded info dictonary (this is a hack, we could reconstruct
* the bencoded stream and recalculate the hash). Will throw a
* InvalidBEncodingException if the given map does not contain a valid
* announce string or info dictonary.
*/
public MetaInfo (Map m) throws InvalidBEncodingException
{
BEValue val = (BEValue)m.get("announce");
if (val == null) {
throw new InvalidBEncodingException("Missing announce string");
}
this.announce = val.getString();
val = (BEValue) m.get("announce-list");
if(val != null) {
this.announceList = new ArrayList<String>();
for(BEValue v : val.getList()) {
//this.announceList.add(v.getString());
List<BEValue> tier = v.getList();
for(BEValue t : tier)
this.announceList.add(t.getString());
}
}
val = (BEValue)m.get("info");
if (val == null) {
throw new InvalidBEncodingException("Missing info map");
}
Map info = val.getMap();
val = (BEValue)info.get("name");
if (val == null) {
throw new InvalidBEncodingException("Missing name string");
}
name = val.getString();
val = (BEValue)info.get("piece length");
if (val == null) {
throw new InvalidBEncodingException("Missing piece length number");
}
piece_length = val.getInt();
val = (BEValue)info.get("pieces");
if (val == null) {
throw new InvalidBEncodingException("Missing piece bytes");
}
piece_hashes = val.getBytes();
val = (BEValue)info.get("length");
if (val != null) {
// Single file case.
length = val.getLong();
files = null;
lengths = null;
} else {
// Multi file case.
val = (BEValue)info.get("files");
if (val == null) {
throw new InvalidBEncodingException(
"Missing length number and/or files list");
}
List list = val.getList();
int size = list.size();
if (size == 0) {
throw new InvalidBEncodingException("zero size files list");
}
files = new ArrayList<List<String>>(size);
lengths = new ArrayList<Long>(size);
long l = 0;
for (int i = 0; i < list.size(); i++) {
Map desc = ((BEValue)list.get(i)).getMap();
val = (BEValue)desc.get("length");
if (val == null) {
throw new InvalidBEncodingException("Missing length number");
}
long len = val.getLong();
lengths.add(len);
l += len;
val = (BEValue)desc.get("path");
if (val == null) {
throw new InvalidBEncodingException("Missing path list");
}
List<BEValue> path_list = val.getList();
int path_length = path_list.size();
if (path_length == 0) {
throw new InvalidBEncodingException(
"zero size file path list");
}
List<String> file = new ArrayList<String>(path_length);
for (BEValue value : path_list) {
file.add(value.getString());
}
files.add(file);
}
length = l;
}
info_hash = calculateInfoHash();
}
/**
* Returns the string representing the URL of the tracker for this torrent.
*/
public String getAnnounce ()
{
return announce;
}
/**
* Returns a list of alternative trackers.
*/
public List<String> getAnnounceList() {
return announceList;
}
/**
* Returns the original 20 byte SHA1 hash over the bencoded info map.
*/
public byte[] getInfoHash ()
{
// XXX - Should we return a clone, just to be sure?
return info_hash;
}
public String getHexInfoHash ()
{
return hexencode(info_hash);
}
/**
* Returns the piece hashes. Only used by storage so package local.
*/
byte[] getPieceHashes ()
{
return piece_hashes;
}
/**
* Returns the requested name for the file or toplevel directory. If it is a
* toplevel directory name getFiles() will return a non-null List of file
* name hierarchy name.
*/
public String getName ()
{
return name;
}
/**
* Returns a list of lists of file name hierarchies or null if it is a
* single name. It has the same size as the list returned by getLengths().
*/
public List getFiles ()
{
// XXX - Immutable?
return files;
}
/**
* Returns a list of Longs indication the size of the individual files, or
* null if it is a single file. It has the same size as the list returned by
* getFiles().
*/
public List getLengths ()
{
// XXX - Immutable?
return lengths;
}
/**
* Returns the number of pieces.
*/
public int getPieces ()
{
return piece_hashes.length / 20;
}
/**
* Return the length of a piece. All pieces are of equal length except for
* the last one (<code>getPieces()-1</code>).
*
* @exception IndexOutOfBoundsException
* when piece is equal to or greater then the number of
* pieces in the torrent.
*/
public int getPieceLength (int piece)
{
int pieces = getPieces();
if (piece >= 0 && piece < pieces - 1) {
return piece_length;
} else if (piece == pieces - 1) {
return (int)(length - piece * piece_length);
} else {
throw new IndexOutOfBoundsException("no piece: " + piece);
}
}
/**
* Checks that the given piece has the same SHA1 hash as the given byte
* array. Returns random results or IndexOutOfBoundsExceptions when the
* piece number is unknown.
*/
public boolean checkPiece (int piece, byte[] bs, int off, int length)
{
// Check digest
MessageDigest sha1;
try {
sha1 = MessageDigest.getInstance("SHA");
} catch (NoSuchAlgorithmException nsae) {
throw new InternalError("No SHA digest available: " + nsae);
}
sha1.update(bs, off, length);
byte[] hash = sha1.digest();
for (int i = 0; i < 20; i++) {
if (hash[i] != piece_hashes[20 * piece + i]) {
return false;
}
}
return true;
}
/**
* Returns the total length of the torrent in bytes.
*/
public long getTotalLength ()
{
return length;
}
@Override
public String toString ()
{
return "MetaInfo[info_hash='" + hexencode(info_hash) + "', announce='"
+ announce + "', announce-list='" + announceList + "', name='" + name + "', files=" + files
+ ", #pieces='" + piece_hashes.length / 20 + "', piece_length='"
+ piece_length + "', length='" + length + "']";
}
/**
* Encode a byte array as a hex encoded string.
*/
private static String hexencode (byte[] bs)
{
StringBuffer sb = new StringBuffer(bs.length * 2);
for (byte element : bs) {
int c = element & 0xFF;
if (c < 16) {
sb.append('0');
}
sb.append(Integer.toHexString(c));
}
return sb.toString();
}
/**
* Creates a copy of this MetaInfo that shares everything except the
* announce URL.
*/
public MetaInfo reannounce (String announce)
{
return new MetaInfo(announce, announceList, name, files, lengths, piece_length,
piece_hashes, length);
}
public byte[] getTorrentData ()
{
if (torrentdata == null) {
Map<String, Object> m = new HashMap<String, Object>();
m.put("announce", announce);
Map info = createInfoMap();
m.put("info", info);
torrentdata = BEncoder.bencode(m);
}
return torrentdata;
}
private Map<String, Object> createInfoMap ()
{
Map<String, Object> info = new HashMap<String, Object>();
info.put("name", name);
info.put("piece length", piece_length);
info.put("pieces", piece_hashes);
if (files == null) {
info.put("length", new Long(length));
} else {
List<Map<String, Object>> l = new ArrayList<Map<String, Object>>();
for (int i = 0; i < files.size(); i++) {
Map<String, Object> file = new HashMap<String, Object>();
file.put("path", files.get(i));
file.put("length", lengths.get(i));
l.add(file);
}
info.put("files", l);
}
return info;
}
private byte[] calculateInfoHash ()
{
Map<String, Object> info = createInfoMap();
byte[] infoBytes = BEncoder.bencode(info);
try {
MessageDigest digest = MessageDigest.getInstance("SHA");
return digest.digest(infoBytes);
} catch (NoSuchAlgorithmException nsa) {
throw new InternalError(nsa.toString());
}
}
}