/* 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.Iterator; import java.util.List; import java.util.Map; import net.i2p.I2PAppContext; import net.i2p.crypto.SHA1; import net.i2p.data.Base64; import net.i2p.util.Log; import org.klomp.snark.bencode.BDecoder; import org.klomp.snark.bencode.BEValue; import org.klomp.snark.bencode.BEncoder; import org.klomp.snark.bencode.InvalidBEncodingException; /** * Note: this class is buggy, as it doesn't propogate custom meta fields into the bencoded * info data, and from there to the info_hash. At the moment, though, it seems to work with * torrents created by I2P-BT, I2PRufus and Azureus. * */ public class MetaInfo { private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(MetaInfo.class); private final String announce; private final byte[] info_hash; private final String name; private final String name_utf8; private final List files; private final List files_utf8; private final List lengths; private final int piece_length; private final byte[] piece_hashes; private final long length; private final Map infoMap; private byte[] torrentdata; MetaInfo(String announce, String name, String name_utf8, List files, List lengths, int piece_length, byte[] piece_hashes, long length) { this.announce = announce; this.name = name; this.name_utf8 = name_utf8; this.files = files; this.files_utf8 = null; this.lengths = lengths; this.piece_length = piece_length; this.piece_hashes = piece_hashes; this.length = length; this.info_hash = calculateInfoHash(); infoMap = null; } /** * 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 { _log.debug("Creating a metaInfo: " + m, new Exception("source")); BEValue val = (BEValue)m.get("announce"); if (val == null) throw new InvalidBEncodingException("Missing announce string"); this.announce = val.getString(); val = (BEValue)m.get("info"); if (val == null) throw new InvalidBEncodingException("Missing info map"); Map info = val.getMap(); infoMap = info; val = (BEValue)info.get("name"); if (val == null) throw new InvalidBEncodingException("Missing name string"); name = val.getString(); val = (BEValue)info.get("name.utf-8"); if (val != null) name_utf8 = val.getString(); else name_utf8 = null; 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; files_utf8 = 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(size); files_utf8 = new ArrayList(size); lengths = new ArrayList(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(new Long(len)); l += len; val = (BEValue)desc.get("path"); if (val == null) throw new InvalidBEncodingException("Missing path list"); List path_list = val.getList(); int path_length = path_list.size(); if (path_length == 0) throw new InvalidBEncodingException("zero size file path list"); List file = new ArrayList(path_length); Iterator it = path_list.iterator(); while (it.hasNext()) file.add(((BEValue)it.next()).getString()); files.add(file); val = (BEValue)desc.get("path.utf-8"); if (val != null) { path_list = val.getList(); path_length = path_list.size(); if (path_length > 0) { file = new ArrayList(path_length); it = path_list.iterator(); while (it.hasNext()) file.add(((BEValue)it.next()).getString()); files_utf8.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 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; } /** * 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 (getPieces()-1). * * @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) { if (true) return fast_checkPiece(piece, bs, off, length); else return orig_checkPiece(piece, bs, off, length); } private boolean orig_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; } private boolean fast_checkPiece(int piece, byte[] bs, int off, int length) { SHA1 sha1 = new SHA1(); 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 + "', 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) { StringBuilder sb = new StringBuilder(bs.length*2); for (int i = 0; i < bs.length; i++) { int c = bs[i] & 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, name, name_utf8, files, lengths, piece_length, piece_hashes, length); } public byte[] getTorrentData() { if (torrentdata == null) { Map m = new HashMap(); m.put("announce", announce); Map info = createInfoMap(); m.put("info", info); torrentdata = BEncoder.bencode(m); } return torrentdata; } private Map createInfoMap() { Map info = new HashMap(); if (infoMap != null) { info.putAll(infoMap); return info; } info.put("name", name); if (name_utf8 != null) info.put("name.utf-8", name_utf8); info.put("piece length", Integer.valueOf(piece_length)); info.put("pieces", piece_hashes); if (files == null) info.put("length", new Long(length)); else { List l = new ArrayList(); for (int i = 0; i < files.size(); i++) { Map file = new HashMap(); file.put("path", files.get(i)); if ( (files_utf8 != null) && (files_utf8.size() > i) ) file.put("path.utf-8", files_utf8.get(i)); file.put("length", lengths.get(i)); l.add(file); } info.put("files", l); } return info; } private byte[] calculateInfoHash() { Map info = createInfoMap(); StringBuilder buf = new StringBuilder(128); buf.append("info: "); for (Iterator iter = info.entrySet().iterator(); iter.hasNext(); ) { Map.Entry entry = (Map.Entry)iter.next(); String key = (String)entry.getKey(); Object val = entry.getValue(); buf.append(key).append('='); if (val instanceof byte[]) buf.append(Base64.encode((byte[])val, true)); else buf.append(val.toString()); } _log.debug(buf.toString()); byte[] infoBytes = BEncoder.bencode(info); //_log.debug("info bencoded: [" + Base64.encode(infoBytes, true) + "]"); try { MessageDigest digest = MessageDigest.getInstance("SHA"); byte hash[] = digest.digest(infoBytes); _log.debug("info hash: [" + net.i2p.data.Base64.encode(hash) + "]"); return hash; } catch(NoSuchAlgorithmException nsa) { throw new InternalError(nsa.toString()); } } }