org.texai.torrent.domainEntity.MetaInfo.java Source code

Java tutorial

Introduction

Here is the source code for org.texai.torrent.domainEntity.MetaInfo.java

Source

/*
 * 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
        }
    }
}