/*
* 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.
*
* Revised by Stephen L. Reed, Dec 22, 2009.
* Reformatted, fixed Checkstyle, Findbugs and PMD violations, and substituted Log4J logger
* for consistency with the Texai project.
*/
package org.texai.torrent.domainEntity;
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.apache.commons.codec.net.URLCodec;
import org.texai.torrent.bencode.BDecoder;
import org.texai.torrent.bencode.BEValue;
import org.texai.torrent.bencode.BEncoder;
import org.texai.torrent.bencode.InvalidBEncodingException;
import org.texai.util.TexaiException;
/** Holds all information gotten from a torrent file. */
public final class MetaInfo {
/** the string representing the URL of the tracker for this torrent */
private final String announceURLString;
/** the original 20 byte SHA1 hash over the bencoded info map */
private final byte[] infoHash;
/** the requested name for the file or toplevel directory */
private final String name;
/** a list of lists of file name hierarchies or null if it is a single name */
private final List<List<String>> files;
/** the list individual file sizes, or null if it is a single file */
private final List<Long> lengths;
/** the length of a piece, all pieces are of equal length except for the last one */
private final int pieceLength;
/** the piece hashes */
private final byte[] pieceHashes;
/** the file length */
private final long length;
/** the bencoded torrent metainfo data */
private byte[] torrentData;
/** Constructs a new MetaInfo instance for a single file torrent.
*
* @param announceURLString the string representing the URL of the tracker for this torrent
* @param name the requested name for the file
* @param pieceLength the length of a piece, all pieces are of equal length except for the last one
* @param pieceHashes the piece hashes
* @param length the file length
*/
public MetaInfo(
final String announceURLString,
final String name,
final int pieceLength,
final byte[] pieceHashes,
final long length) {
this(
announceURLString,
name,
null, // files
null, // lengths
pieceLength,
pieceHashes,
length);
}
/** Constructs a new MetaInfo instance for a multi-file torrent.
*
* @param announceURLString the string representing the URL of the tracker for this torrent
* @param name the requested name for the toplevel directory
* @param files a list of lists of file name hierarchies
* @param lengths the list individual file sizes
* @param pieceLength the length of a piece, all pieces are of equal length except for the last one
* @param pieceHashes the piece hashes
* @param length the file length
*/
public MetaInfo(
final String announceURLString,
final String name,
final List<List<String>> files,
final List<Long> lengths,
final int pieceLength,
final byte[] pieceHashes,
final long length) {
//Preconditions
assert announceURLString != null : "announceURLString must not be null";
assert !announceURLString.isEmpty() : "announceURLString must not be empty";
assert name != null : "name must not be null";
assert !name.isEmpty() : "name must not be empty";
assert pieceHashes != null : "pieceHashes must not be null";
assert length > 0 : "length must be positive";
this.announceURLString = announceURLString;
this.name = name;
this.files = files;
this.lengths = lengths;
this.pieceLength = pieceLength;
this.pieceHashes = pieceHashes;
this.length = length;
this.infoHash = calculateInfoHash();
}
/** Creates a new MetaInfo from the given InputStream. The InputStream must
* start with a correctly bencoded dictonary describing the torrent.
*
* @param inputStream the given InputStream
* @throws IOException if an input/output error occurs
*/
public MetaInfo(final InputStream inputStream) throws IOException {
this(new BDecoder(inputStream));
}
/** Creates a new MetaInfo from the given BDecoder. The BDecoder must have a
* complete dictionary describing the torrent.
*
* @param bDecoder the given BDecoder
* @throws IOException if an input/output error occurs
*/
@SuppressWarnings("unchecked")
public MetaInfo(final BDecoder bDecoder) throws IOException {
// Note that evaluation order matters here...
this(bDecoder.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.
*
* @param map the map of BEValues
* @throws InvalidBEncodingException if an error occurs
*/
public MetaInfo(final Map<String, BEValue> map) throws InvalidBEncodingException {
BEValue val = map.get("announce");
if (val == null) {
throw new InvalidBEncodingException("Missing announce string");
}
this.announceURLString = val.getString();
val = map.get("info");
if (val == null) {
throw new InvalidBEncodingException("Missing info map");
}
@SuppressWarnings("unchecked")
final Map<String, BEValue> info = val.getMap();
val = info.get("name");
if (val == null) {
throw new InvalidBEncodingException("Missing name string");
}
name = val.getString();
val = info.get("piece length");
if (val == null) {
throw new InvalidBEncodingException("Missing piece length number");
}
pieceLength = val.getInt();
val = info.get("pieces");
if (val == null) {
throw new InvalidBEncodingException("Missing piece bytes");
}
pieceHashes = val.getBytes();
val = info.get("length");
if (val == null) {
// Multi file case.
val = info.get("files");
if (val == null) {
throw new InvalidBEncodingException(
"Missing length number and/or files list");
}
final List<BEValue> list = val.getList();
final int size = list.size();
if (size == 0) {
throw new InvalidBEncodingException("zero size files list");
}
files = new ArrayList<>(size);
lengths = new ArrayList<>(size);
long length1 = 0;
for (BEValue list1 : list) {
@SuppressWarnings(value = "unchecked")
final Map<String, BEValue> desc = list1.getMap();
val = desc.get("length");
if (val == null) {
throw new InvalidBEncodingException("Missing length number");
}
final long len = val.getLong();
lengths.add(len);
length1 += len;
val = desc.get("path");
if (val == null) {
throw new InvalidBEncodingException("Missing path list");
}
final List<BEValue> path_list = val.getList();
final int path_length = path_list.size();
if (path_length == 0) {
throw new InvalidBEncodingException(
"zero size file path list");
}
final List<String> file = new ArrayList<>(path_length);
for (BEValue value : path_list) {
file.add(value.getString());
}
files.add(file);
}
length = length1;
} else {
// Single file case.
length = val.getLong();
files = null;
lengths = null;
}
infoHash = calculateInfoHash();
}
/** Gets the string representing the URL of the tracker for this torrent.
*
* @return the string representing the URL of the tracker for this torrent
*/
public String getAnnounceURLString() {
return announceURLString;
}
/** Returns the original 20 byte SHA1 hash over the bencoded info map.
*
* @return 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 infoHash;
}
/** Gets the url encoded info hash.
*
* @return the url encoded info hash
*/
public String getURLEncodedInfoHash() {
return new String((new URLCodec()).encode(infoHash));
}
/** Returns the piece hashes. Only used by storage so package local.
*
* @return the piece hashes
*/
public byte[] getPieceHashes() {
return pieceHashes;
}
/** 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.
*
* @return the requested name for the file or toplevel directory
*/
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().
*
* @return a list of lists of file name hierarchies or null if it is a
* single name
*/
public List<List<String>> getFiles() {
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().
*
* @return the list of the individual file sizes, or null if a single file
*/
public List<Long> getLengths() {
// XXX - Immutable?
return lengths;
}
/** Returns the number of pieces.
*
* @return the number of pieces
*/
public int getNbrPieces() {
return pieceHashes.length / 20;
}
/** Return the length of a piece. All pieces are of equal length except for
* the last one (<code>getPieces()-1</code>).
*
* @param pieceIndex the piece index
* @return the length of a piece
*/
public int getPieceLength(final int pieceIndex) {
final int nbrPieces = getNbrPieces();
if (pieceIndex >= 0 && pieceIndex < nbrPieces - 1) {
return pieceLength;
} else if (pieceIndex == nbrPieces - 1) {
return (int) (length - (long) pieceIndex * pieceLength);
} else {
throw new IndexOutOfBoundsException("no piece: " + pieceIndex);
}
}
/** 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.
*
* @param pieceIndex the piece index
* @param pieceBuffer the array of bytes
* @param offset the offset to start from in the array of bytes
* @param length the number of bytes to use, starting at the offset
* @return whether the given piece has the same SHA1 hash as the given byte array
*/
public boolean checkPiece(
final int pieceIndex,
final byte[] pieceBuffer,
final int offset,
final int length) {
// Check digest
final MessageDigest sha1;
try {
sha1 = MessageDigest.getInstance("SHA");
} catch (NoSuchAlgorithmException ex) {
throw new TexaiException(ex);
}
sha1.update(pieceBuffer, offset, length);
final byte[] hash = sha1.digest();
for (int i = 0; i < 20; i++) {
if (hash[i] != pieceHashes[20 * pieceIndex + i]) {
return false;
}
}
return true;
}
/** Gets the total length of the torrent in bytes.
*
* @return the total length of the torrent in bytes
*/
public long getTotalLength() {
return length;
}
/** Returns a string representation of this object.
*
* @return a string representation of this object
*/
@Override
public String toString() {
return "MetaInfo[info_hash='" +
getURLEncodedInfoHash() +
"', announce='" +
announceURLString +
"', name='" + name +
"', files=" + files +
", lengths=" + lengths +
", #pieces='" +
pieceHashes.length / 20 +
"', piece_length='" + pieceLength +
"', length='" +
length +
"']";
}
/** Encode a byte array as a hex encoded string.
*
* @param byteArray the byte array
* @return a hex encoded string
*/
private static String hexEncode(final byte[] byteArray) {
final StringBuffer stringBuffer = new StringBuffer(byteArray.length * 2);
for (byte element : byteArray) {
final int hexDigit = element & 0xFF;
if (hexDigit < 16) {
stringBuffer.append('0');
}
stringBuffer.append(Integer.toHexString(hexDigit));
}
return stringBuffer.toString();
}
/** Creates a copy of this MetaInfo that shares everything except the announce URL.
*
* @param announce the string representing the URL of the tracker for this torrent
* @return a copy of this MetaInfo that shares everything except the announce URL
*/
public MetaInfo reannounce(final String announce) {
return new MetaInfo(
announce,
name,
files,
lengths,
pieceLength,
pieceHashes,
length);
}
/** Gets the bencoded torrent metainfo data.
*
* @return the bencoded torrent metainfo data
*/
public byte[] getTorrentData() {
if (torrentData == null) {
final Map<String, Object> map = new HashMap<>();
map.put("announce", announceURLString);
final Map<String, Object> info = createInfoMap();
map.put("info", info);
System.out.println("torrent data: " + map);
torrentData = BEncoder.bencode(map);
}
return torrentData;
}
/** Creates the info map.
*
* @return the info map
*/
private Map<String, Object> createInfoMap() {
final Map<String, Object> info = new HashMap<>();
info.put("name", name);
info.put("piece length", pieceLength);
info.put("pieces", pieceHashes);
if (files == null) {
info.put("length", length);
} else {
final List<Map<String, Object>> mapList = new ArrayList<>();
for (int i = 0; i < files.size(); i++) {
final Map<String, Object> fileMap = new HashMap<>();
fileMap.put("path", files.get(i));
fileMap.put("length", lengths.get(i));
mapList.add(fileMap);
}
info.put("files", mapList);
}
return info;
}
/** Calculates the info hash.
*
* @return the info hash
*/
private byte[] calculateInfoHash() {
final Map<String, Object> info = createInfoMap();
final byte[] infoBytes = BEncoder.bencode(info);
try {
final MessageDigest digest = MessageDigest.getInstance("SHA");
return digest.digest(infoBytes);
} catch (final NoSuchAlgorithmException nsa) {
throw new InternalError(nsa.toString()); // NOPMD
}
}
/** Returns the hex-encoded digest of the piece hashes.
*
* @return the hex-encoded digest of the piece hashes
*/
public String getDigestedPieceHashes() {
try {
final MessageDigest digest = MessageDigest.getInstance("SHA");
return hexEncode(digest.digest(pieceHashes));
} catch (final NoSuchAlgorithmException nsa) {
throw new InternalError(nsa.toString()); // NOPMD
}
}
}