fr.letroll.ttorrentandroid.common.Torrent.java Source code

Java tutorial

Introduction

Here is the source code for fr.letroll.ttorrentandroid.common.Torrent.java

Source

/**
 * Copyright (C) 2011-2012 Turn, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package fr.letroll.ttorrentandroid.common;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.Charsets;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.WillNotClose;

import fr.letroll.ttorrentandroid.bcodec.BEValue;
import fr.letroll.ttorrentandroid.bcodec.BytesBDecoder;
import fr.letroll.ttorrentandroid.bcodec.BytesBEncoder;
import fr.letroll.ttorrentandroid.bcodec.StreamBDecoder;
import fr.letroll.ttorrentandroid.bcodec.StreamBEncoder;

/**
 * A torrent file tracked by the controller's BitTorrent tracker.
 *
 * <p>
 * This class represents an active torrent on the tracker. The torrent
 * information is kept in-memory, and is created from the byte blob one would
 * usually find in a <tt>.torrent</tt> file.
 * </p>
 *
 * <p>
 * Each torrent also keeps a repository of the peers seeding and leeching this
 * torrent from the tracker.
 * </p>
 *
 * @author mpetazzoni
 * @see <a href="http://wiki.theory.org/BitTorrentSpecification#Metainfo_File_Structure">Torrent meta-info file structure specification</a>
 */
public class Torrent {

    private static final Logger logger = LoggerFactory.getLogger(Torrent.class);
    /** Torrent file piece length (in bytes), we use 512 kB. */
    public static final int PIECE_HASH_SIZE = 20;
    /** The query parameters encoding when parsing byte strings. */
    public static final Charset BYTE_ENCODING = Charsets.ISO_8859_1;
    public static final String BYTE_ENCODING_NAME = BYTE_ENCODING.name();

    /**
     *
     * @author dgiffin
     * @author mpetazzoni
     */
    public static class TorrentFile {

        public final String path;
        public final long size;

        public TorrentFile(@Nonnull String path, @Nonnegative long size) {
            this.path = path;
            this.size = size;
        }
    }

    private final Map<String, BEValue> decoded;
    private final Map<String, BEValue> decoded_info;
    private final byte[] info_hash;
    private final List<List<URI>> trackers = new ArrayList<List<URI>>();
    private final long creationTime;
    private final String comment;
    private final String createdBy;
    private final String name;
    private final long size;
    private final List<TorrentFile> files = new ArrayList<TorrentFile>();
    private final int pieceLength;
    private final byte[] piecesHashes;

    @Nonnull
    private static Map<String, BEValue> load(@Nonnull File file) throws IOException {
        InputStream in = FileUtils.openInputStream(file);
        try {
            return new StreamBDecoder(in).bdecodeMap().getMap();
        } finally {
            IOUtils.closeQuietly(in);
        }
    }

    /**
     * Load a torrent from the given torrent file.
     *
     * @param torrent The abstract {@link java.io.File} object representing the
     * <tt>.torrent</tt> file to load.
     * @throws java.io.IOException When the torrent file cannot be read.
     */
    public Torrent(@Nonnull File torrent) throws IOException, URISyntaxException {
        this(load(torrent));
    }

    /**
     * Create a new torrent from meta-info binary data.
     *
     * Parses the meta-info data (which should be B-encoded as described in the
     * BitTorrent specification) and create a Torrent object from it.
     *
     * @param torrent The meta-info byte data.
     * @throws java.io.IOException When the info dictionary can't be read or
     * encoded and hashed back to create the torrent's SHA-1 hash.
     */
    public Torrent(@Nonnull byte[] torrent) throws IOException, URISyntaxException {
        this(new BytesBDecoder(torrent).bdecodeMap().getMap());
    }

    public Torrent(@Nonnull Map<String, BEValue> torrent) throws IOException, URISyntaxException {
        this.decoded = torrent;
        this.decoded_info = this.decoded.get("info").getMap();

        BytesBEncoder encoder = new BytesBEncoder();
        encoder.bencode(decoded_info);
        this.info_hash = Torrent.hash(encoder.toByteArray());

        /**
         * Parses the announce information from the decoded meta-info
         * structure.
         *
         * <p>
         * If the torrent doesn't define an announce-list, use the mandatory
         * announce field value as the single tracker in a single announce
         * tier.  Otherwise, the announce-list must be parsed and the trackers
         * from each tier extracted.
         * </p>
         *
         * @see <a href="http://bittorrent.org/beps/bep_0012.html">BitTorrent BEP#0012 "Multitracker Metadata Extension"</a>
         */
        Set<URI> allTrackers = new HashSet<URI>();

        if (this.decoded.containsKey("announce-list")) {
            List<BEValue> in_tiers = this.decoded.get("announce-list").getList();
            for (BEValue in_tier_bvalue : in_tiers) {
                List<BEValue> in_tier = in_tier_bvalue.getList();
                if (in_tier.isEmpty())
                    continue;

                List<URI> out_tier = new ArrayList<URI>();
                for (BEValue in_tracker : in_tier) {
                    URI uri = new URI(in_tracker.getString());

                    // Make sure we're not adding duplicate trackers.
                    if (!allTrackers.contains(uri)) {
                        out_tier.add(uri);
                        allTrackers.add(uri);
                    }
                }

                // Only add the tier if it's not empty.
                if (!out_tier.isEmpty())
                    this.trackers.add(out_tier);
            }
        } else if (this.decoded.containsKey("announce")) {
            URI tracker = new URI(this.decoded.get("announce").getString());
            // Build a single-tier announce list.
            this.trackers.add(Arrays.asList(tracker));
        }

        this.creationTime = this.decoded.containsKey("creation date")
                ? this.decoded.get("creation date").getLong() * 1000
                : -1L;
        this.comment = this.decoded.containsKey("comment") ? this.decoded.get("comment").getString() : null;
        this.createdBy = this.decoded.containsKey("created by") ? this.decoded.get("created by").getString() : null;
        this.name = this.decoded_info.get("name").getString();

        // Parse multi-file torrent file information structure.
        if (this.decoded_info.containsKey("files")) {
            for (BEValue file : this.decoded_info.get("files").getList()) {
                Map<String, BEValue> fileInfo = file.getMap();
                StringBuilder path = new StringBuilder(this.name);
                for (BEValue pathElement : fileInfo.get("path").getList()) {
                    path.append(File.separator).append(pathElement.getString());
                }
                this.files.add(new TorrentFile(path.toString(), fileInfo.get("length").getLong()));
            }
        } else {
            // For single-file torrents, the name of the torrent is
            // directly the name of the file.
            this.files.add(new TorrentFile(this.name, this.decoded_info.get("length").getLong()));
        }

        // Calculate the total size of this torrent from its files' sizes.
        long size = 0;
        for (TorrentFile file : this.files)
            size += file.size;
        this.size = size;

        this.pieceLength = this.decoded_info.get("piece length").getInt();
        this.piecesHashes = this.decoded_info.get("pieces").getBytes();

        if (this.piecesHashes.length / Torrent.PIECE_HASH_SIZE * (long) this.pieceLength < this.size) {
            throw new IllegalArgumentException(
                    "Torrent size does not " + "match the number of pieces and the piece size!");
        }

        logger.info("{}-file torrent information:", this.isMultifile() ? "Multi" : "Single");
        logger.info("  Torrent name: {}", this.name);
        logger.info("  Announced at:" + (trackers.isEmpty() ? " Seems to be trackerless" : ""));
        for (int i = 0; i < this.trackers.size(); i++) {
            List<URI> tier = this.trackers.get(i);
            for (int j = 0; j < tier.size(); j++) {
                logger.info("    {}{}", (j == 0 ? String.format("%2d. ", i + 1) : "    "), tier.get(j));
            }
        }

        if (this.creationTime > 0)
            logger.info("  Created on..: {}", new Date(this.creationTime));
        if (this.comment != null)
            logger.info("  Comment.....: {}", this.comment);
        if (this.createdBy != null)
            logger.info("  Created by..: {}", this.createdBy);

        if (this.isMultifile()) {
            logger.info("  Found {} file(s) in multi-file torrent structure.", this.files.size());
            int i = 0;
            for (TorrentFile file : this.files) {
                logger.debug("    {}. {} ({} byte(s))",
                        new Object[] { String.format("%2d", ++i), file.path, String.format("%,d", file.size) });
            }
        }

        logger.info("  Pieces......: {} piece(s) ({} byte(s)/piece)",
                (this.size / this.decoded_info.get("piece length").getInt()) + 1,
                this.decoded_info.get("piece length").getInt());
        logger.info("  Total size..: {} byte(s)", String.format("%,d", this.size));
    }

    /**
     * Get this torrent's name.
     *
     * <p>
     * For a single-file torrent, this is usually the name of the file. For a
     * multi-file torrent, this is usually the name of a top-level directory
     * containing those files.
     * </p>
     */
    public String getName() {
        return this.name;
    }

    /**
     * Get this torrent's comment string.
     */
    public String getComment() {
        return this.comment;
    }

    /**
     * Get this torrent's creator (user, software, whatever...).
     */
    public String getCreatedBy() {
        return this.createdBy;
    }

    /**
     * Get the total size of this torrent.
     */
    @Nonnegative
    public long getSize() {
        return this.size;
    }

    @Nonnull
    public List<TorrentFile> getFiles() {
        return files;
    }

    /**
     * Get the file names from this torrent.
     *
     * @return The list of relative filenames of all the files described in
     * this torrent.
     */
    public List<String> getFilenames() {
        List<String> filenames = new ArrayList<String>(files.size());
        for (TorrentFile file : this.files)
            filenames.add(file.path);
        return filenames;
    }

    @Nonnegative
    public int getPieceCount() {
        return (int) (Math.ceil((double) getSize() / getPieceLength()));
    }

    @Nonnegative
    public long getPieceOffset(@Nonnegative int index) {
        return (long) getPieceLength() * (long) index;
    }

    @Nonnegative
    public int getPieceLength() {
        return pieceLength;
    }

    /**
     * Returns the size, in bytes, of the given piece.
     *
     * <p>
     * All pieces, except the last one, are expected to have the same size.
     * </p>
     */
    @Nonnegative
    public int getPieceLength(@Nonnegative int index) {
        // The last piece may be shorter than the torrent's global piece
        // length. Let's make sure we get the right piece length in any
        // situation.
        if (index < getPieceCount() - 1)
            return getPieceLength();
        return (int) (getSize() % getPieceLength());
    }

    @Nonnull
    @SuppressWarnings("EI_EXPOSE_REP")
    public byte[] getPiecesHashes() {
        return piecesHashes;
    }

    @Nonnull
    public byte[] getPieceHash(@Nonnegative int index) {
        byte[] hashes = getPiecesHashes();
        int offset = index * PIECE_HASH_SIZE;
        return Arrays.copyOfRange(hashes, offset, offset + PIECE_HASH_SIZE);
    }

    public boolean isPieceValid(@Nonnegative int index, @Nonnull ByteBuffer data) {
        if (data.remaining() != getPieceLength(index))
            throw new IllegalArgumentException("Validating piece " + index + " expected " + getPieceLength(index)
                    + ", not " + data.remaining());
        MessageDigest digest = DigestUtils.getSha1Digest();
        digest.update(data);
        return Arrays.equals(digest.digest(), getPieceHash(index));
    }

    /**
     * Tells whether this torrent is multi-file or not.
     */
    public boolean isMultifile() {
        return this.files.size() > 1;
    }

    /**
     * Return the hash of the B-encoded meta-info structure of this torrent.
     */
    @Nonnull
    @SuppressWarnings("EI_EXPOSE_REP")
    public byte[] getInfoHash() {
        return this.info_hash;
    }

    /**
     * Get this torrent's info hash (as an hexadecimal-coded string).
     */
    @Nonnull
    public String getHexInfoHash() {
        return TorrentUtils.toHex(this.info_hash);
    }

    /**
     * Return the trackers for this torrent.
     */
    @Nonnull
    public List<List<URI>> getAnnounceList() {
        return this.trackers;
    }

    /**
     * Returns the number of trackers for this torrent.
     */
    @Nonnegative
    public int getTrackerCount() {
        int count = 0;
        for (List<URI> tier : getAnnounceList())
            count += tier.size();
        return count;
    }

    @Nonnull
    public byte[] toByteArray() throws IOException {
        BytesBEncoder encoder = new BytesBEncoder();
        encoder.bencode(decoded);
        return encoder.toByteArray();
    }

    /**
     * Save this torrent meta-info structure into a .torrent file.
     *
     * @param output The stream to write to.
     * @throws java.io.IOException If an I/O error occurs while writing the file.
     */
    public void save(@Nonnull @WillNotClose OutputStream output) throws IOException {
        StreamBEncoder encoder = new StreamBEncoder(output);
        encoder.bencode(decoded);
        output.flush();
    }

    /**
     * Return a human-readable representation of this torrent object.
     *
     * <p>
     * The torrent's name is used.
     * </p>
     */
    @Override
    public String toString() {
        return getName();
    }

    @Nonnull
    public static byte[] hash(@Nonnull byte[] data) {
        return DigestUtils.sha1(data);
    }
}